Skip to content

Examples

Date Picker

DatePickerApp ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔    -  -   ▼  ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

from textual.app import App, ComposeResult
from textual_timepiece.pickers import DatePicker


class DatePickerApp(App[None]):

    def compose(self) -> ComposeResult:
        yield DatePicker()

    def on_date_picker_date_changed(self, message: DatePicker.Changed) -> None:
        message.stop()
        if message.date:
            msg = f"Date changed to {message.date.format_common_iso()}."
        else:
            msg = "Date was removed."

        self.notify(msg)


if __name__ == "__main__":
    DatePickerApp().run()

Date Range

DateRangeApp ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ 2025-02-05🔒    -  -   ▼  ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

from textual.app import App, ComposeResult
from textual_timepiece.pickers import DateRangePicker
from whenever import Date, weeks


class DateRangeApp(App[None]):

    def compose(self) -> ComposeResult:
        yield DateRangePicker(Date(2025, 2, 5), date_range=weeks(1))


if __name__ == "__main__":
    DateRangeApp().run()

Date Select

DateSelectApp ────────────────────────────────────── June 2025 MonTueWedThuFriSatSun   1   2  3  4  5  6  7  8   9 10 11 12 13 14 15  16 17 18 19 20 21 22  23 24 25 26 27 28 29  30 ──────────────────────────────────────

from textual.app import App, ComposeResult
from textual import on
from textual.widgets import Label
from textual_timepiece.pickers import DatePicker, DateSelect
from whenever import Date, days


class DateSelectApp(App[None]):

    def compose(self) -> ComposeResult:
        yield DateSelect(Date.today_in_system_tz(), date_range=days(3))
        yield Label(variant="accent")

    @on(DateSelect.StartChanged)
    @on(DateSelect.EndChanged)
    def on_date_changed(self, message: DateSelect.StartChanged | DateSelect.EndChanged) -> None:
        new_content = f"  {message.widget.date} - {message.widget.end_date}  "
        self.query_one(Label).update(new_content)


if __name__ == "__main__":
    DateSelectApp().run()

DateTime Range Picker

DTPickerRangeApp ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ 2025-06-0708:45:26🔓    -  -     :  :   ▼  ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ Started timer!

from textual.app import App, ComposeResult
from textual_timepiece.pickers import DateTimeRangePicker
from textual import on
from whenever import Date, SystemDateTime


class DTPickerRangeApp(App[None]):

    def on_mount(self) -> None:
        self.query_one(DateTimeRangePicker).disable_end()
        self.set_timer(10, self.stop_timer)
        self.notify("Started timer!")

    def compose(self) -> ComposeResult:
        yield DateTimeRangePicker(SystemDateTime.now().local())

    def stop_timer(self) -> None:
        dt_range = self.query_one(DateTimeRangePicker).disable_end(disable=False)
        dt_range.end_dt = SystemDateTime.now().local()


if __name__ == "__main__":
    DTPickerRangeApp().run()

Activity Heatmap

ActivityApp │││││││││││││││││││││││││ <<  < 2025 >  >> ││││││││││││││││││││││││││ Mon████████████████████████████████████████████████ Tue████████████████████████████████████████████████ Wed██████████████████████████████████████████████████ Thu██████████████████████████████████████████████████ Fri██████████████████████████████████████████████████ Sat██████████████████████████████████████████████████ Sun██████████████████████████████████████████████████  1 2 3 4 5 6 7 8 910111213141516171819202122232425 JanFebMarAprMayJun

import random
from collections import defaultdict

from textual.app import App, ComposeResult
from textual_timepiece.activity_heatmap import ActivityHeatmap, HeatmapManager


class ActivityApp(App[None]):
    def _on_heatmap_manager_year_changed(
        self,
        message: HeatmapManager.YearChanged,
    ) -> None:
        message.stop()
        self.set_heatmap_data(message.year)

    def retrieve_data(self, year: int) -> ActivityHeatmap.ActivityData:
        """Placeholder example on how the data could be generated."""
        random.seed(year)
        template = ActivityHeatmap.generate_empty_activity(year)
        return defaultdict(
            lambda: 0,
            {
                day: random.randint(6000, 20000)
                for week in template
                for day in week
                if day
            },
        )

    def set_heatmap_data(self, year: int) -> None:
        """Sets the data based on the current data."""
        self.query_one(ActivityHeatmap).values = self.retrieve_data(year)

    def _on_mount(self) -> None:
        self.set_heatmap_data(2025)

    def compose(self) -> ComposeResult:
        yield HeatmapManager(2025)


if __name__ == "__main__":
    ActivityApp().run()

Single Line Picker

MiniPickerApp 2025-06-0708:45:2600:00:00🔓2025-06-0708:45:26 ▼ 

from textual.app import App, ComposeResult
from textual_timepiece.pickers import DateTimeDurationPicker
from whenever import Date, SystemDateTime, weeks


class MiniPickerApp(App[None]):

    def compose(self) -> ComposeResult:
        yield DateTimeDurationPicker(SystemDateTime.now().local(), classes="mini")


if __name__ == "__main__":
    MiniPickerApp().run()

Vertical Timeline

TimelineApp  ──── ────────────────────────────────────────────────────────────────  ──── ────────────────────────────────────────────────────────────────  ──── ────────────────────────────────────────────────────────────────  ──── ──────────────────────────────────────────────────────────────── 01:00 ────────────────────────────────────────────────────────────────  ──── ────────────────────────────────────────────────────────────────  ──── ────────────────────────────────────────────────────────────────  ──── ──────────────────────────────────────────────────────────────── 02:00 ────────────────────────────────────────────────────────────────  ──── ────────────────────────────────────────────────────────────────  ──── ────────────────────────────────────────────────────────────────  ──── ──────────────────────────────────────────────────────────────── 03:00 ────────────────────────────────────────────────────────────────  ──── ────────────────────────────────────────────────────────────────  ──── ────────────────────────────────────────────────────────────────  ──── ──────────────────────────────────────────────────────────────── 04:00 ────────────────────────────────────────────────────────────────  ──── ────────────────────────────────────────────────────────────────  ──── ────────────────────────────────────────────────────────────────  ──── ──────────────────────────────────────────────────────────────── 05:00 ────────────────────────────────────────────────────────────────  ──── ────────────────────────────────────────────────────────────────  ──── ────────────────────────────────────────────────────────────────  ──── ────────────────────────────────────────────────────────────────

from __future__ import annotations

from textual import on, work
from textual.app import ComposeResult, App
from textual.containers import Center, Middle
from textual.screen import ModalScreen
from textual.widgets import Input

from textual_timepiece.timeline import RuledVerticalTimeline
from textual_timepiece.timeline import VerticalTimeline


class NamingModal(ModalScreen[str]):
    """Modal screen for naming entries."""

    DEFAULT_CSS = """\
    NamingModal {
        align: center middle;
        Center{
            border: tall $primary;
            border-top: panel $primary;
            border-title-align: center;
            border-title-style: bold;
            min-width: 34;
            width: 50%;
            min-height: 5;
            height: 20%;
        }
        Input {
            min-width: 30;
        }
    }
    """
    BINDINGS = [("escape", "dismiss")]

    def compose(self) -> ComposeResult:
        with Center() as center, Middle():
            center.border_title = "What would you like to name the entry?"
            yield Input(
                placeholder="Name",
                valid_empty=False,
                validate_on=["submitted"],
            )

    def on_input_submitted(self, message: Input.Submitted) -> None:
        message.stop()
        if message.input.is_valid:
            self.dismiss(message.value)


class TimelineApp(App[None]):
    """Example of how timelines could be implemented."""

    def compose(self) -> ComposeResult:
        yield RuledVerticalTimeline(3)

    @work(name="naming-worker")
    async def on_vertical_timeline_created(
        self,
        message: VerticalTimeline.Created,
    ) -> None:
        result = await self.push_screen_wait(NamingModal())
        message.entry.remove_class("-mime").update(result or "To Be Named")

    def on_vertical_timeline_deleted(
        self,
        message: VerticalTimeline.Deleted,
    ) -> None:
        self.notify(f"Successfully deleted {message.entry.visual!s}!")


if __name__ == "__main__":
    TimelineApp().run()

Horizontal Timeline

TimelineApp ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔  - 2 +  ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ ││││││││││││││││││││││01:00│││││││││││02:00│││││││││││03:00│││││││││││04:00│││││ ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ │││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││ One│││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││ │││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││ ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ │││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││ Two│││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││ │││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││ │││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││ ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ │││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││ Three│││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││ │││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││ │││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││││ ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

from __future__ import annotations

from textual.reactive import var

from textual import on, work
from textual.app import ComposeResult, App
from textual.containers import Center, Horizontal, HorizontalGroup, Middle
from textual.screen import ModalScreen
from textual.validation import Integer
from textual.widgets import Button, Input, Label, Static

from textual_timepiece.timeline import RuledHorizontalTimeline
from textual_timepiece.timeline import HorizontalTimeline


NUMBERS: tuple[str, ...] = [
    "Zero",
    "One",
    "Two",
    "Three",
    "Four",
    "Five",
    "Six",
    "Seven",
    "Eight",
    "Nine",
    "Ten",
]


def header_factory(index: int) -> Label:
    return Label(
        f"{NUMBERS[index]}",
        variant="primary" if index % 2 == 0 else "secondary",
        classes="header",
    )


class TimelineApp(App[None]):
    """Example of how horizontal timelines could be implemented."""

    DEFAULT_CSS = """\
    Screen {
        Input {
            width: 1fr;
        }
        HorizontalRuler {
            padding-left: 11;  # Compensating for header size.
        }
        Label.header {
            color: auto;
            border: hkey $secondary;
            min-width: 11;
            max-width: 11;
            height: 100%;
            text-align: center;
            content-align: center middle;
            text-style: bold;
            padding: 0 2 0 2;
        }
    }
    """

    layers = var[int](2, init=False)
    """Total amount of timelines to display."""

    def compose(self) -> ComposeResult:
        with HorizontalGroup(id="layer-controls"):
            yield Button.warning("-", id="subtract")
            yield Input(
                "2",
                "Total Timelines",
                type="integer",
                valid_empty=False,
                validate_on=["changed"],
                validators=Integer(1, 10),
                tooltip=(
                    "Total Timelines Present\n"
                    "[b]Maximum[/]: 10\n"
                    "[b]Minimum[/]: 1"
                ),
            )
            yield Button.success("+", id="add")
        yield (timeline := RuledHorizontalTimeline(
            3,
            header_factory=header_factory,
        ).data_bind(total=TimelineApp.layers))
        timeline.length = 392

    def _on_input_changed(self, message: Input.Changed) -> None:
        if message.input.is_valid:
            self.layers = int(message.value)

    def _on_button_pressed(self, message: Button.Pressed) -> None:
        self.layers -= 1 if message.button.id == "subtract" else -1

    def watch_layers(self, total: int) -> None:
        self.query_one("#subtract").disabled = total == 1
        self.query_one("#add").disabled = total == 10
        with (input_widget := self.query_one(Input)).prevent(Input.Changed):
            input_widget.value = str(total)


if __name__ == "__main__":
    TimelineApp().run()