Examples
Date Picker¶
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¶
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¶
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¶
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¶
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¶
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¶
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¶
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()