Skip to content

Pickers

Note

These widgets are composed through a combination of selector and input widgets. To fully understand the picker functionality, it is recommended to read the aforementioned pages.

Info

All widgets have a .mini CSS class which you can assign to these widgets, to convert them to a single line.


DatePicker

DatePickerApp ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ 2025-03-10 ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ──────────────────────────────────────── March 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  31 ────────────────────────────────────────

textual_timepiece.pickers.DatePicker

Bases: BasePicker[DateInput, Date, DateOverlay]

Single date picker with an input and overlay.

PARAMETER DESCRIPTION
date

Initial date for the picker.

TYPE: Date | None DEFAULT: None

name

Name for the widget.

TYPE: str | None DEFAULT: None

id

DOM identifier for widget.

TYPE: str | None DEFAULT: None

classes

Classes to add the widget.

TYPE: str | None DEFAULT: None

date_range

Date range to lock the widget to.

TYPE: DateDelta | None DEFAULT: None

disabled

Disable the widget.

TYPE: bool DEFAULT: False

validator

A callable that will validate and adjust the date if needed.

TYPE: DateValidator | None DEFAULT: None

tooltip

A tooltip to show when hovering the widget.

TYPE: str | None DEFAULT: None

Examples:

>>> def limit_dates(date: Date | None) -> Date | None:
>>>     if date is None:
>>>         return None
>>>     return min(Date(2025, 2, 20), max(Date(2025, 2, 6), date))
>>> yield DatePicker(validator=limit_dates)
>>> yield DatePicker(
>>>     Date.today_in_system_tz(),
>>>     date_range=DateDelta(days=5),
>>> )
CLASS DESCRIPTION
DateChanged

Message sent when the date changed.

METHOD DESCRIPTION
action_clear

Clear the date.

to_default

Reset the date to today.

ATTRIBUTE DESCRIPTION
DEFAULT_CSS

TYPE: str

BINDINGS

All bindings for BasePicker classes.

TYPE: list[BindingType]

DateValidator

Callable type for validating date types.

TYPE: TypeAlias

date

Current date for the picker. This is bound to every other subwidget.

Source code in src/textual_timepiece/pickers/_date_picker.py
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
class DatePicker(BasePicker[DateInput, Date, DateOverlay]):
    """Single date picker with an input and overlay.

    Params:
        date: Initial date for the picker.
        name: Name for the widget.
        id: DOM identifier for widget.
        classes: Classes to add the widget.
        date_range: Date range to lock the widget to.
        disabled: Disable the widget.
        validator: A callable that will validate and adjust the date if needed.
        tooltip: A tooltip to show when hovering the widget.

    Examples:
        >>> def limit_dates(date: Date | None) -> Date | None:
        >>>     if date is None:
        >>>         return None
        >>>     return min(Date(2025, 2, 20), max(Date(2025, 2, 6), date))
        >>> yield DatePicker(validator=limit_dates)

        >>> yield DatePicker(
        >>>     Date.today_in_system_tz(),
        >>>     date_range=DateDelta(days=5),
        >>> )
    """

    @dataclass
    class DateChanged(BaseMessage):
        """Message sent when the date changed."""

        widget: DatePicker
        date: Date | None

    BINDING_GROUP_TITLE = "Date Picker"
    ALIAS = "date"

    DateValidator: TypeAlias = Callable[[Date | None], Date | None]
    """Callable type for validating date types."""

    date = var[Date | None](None, init=False)
    """Current date for the picker. This is bound to every other subwidget."""

    def __init__(
        self,
        date: Date | None = None,
        name: str | None = None,
        id: str | None = None,
        classes: str | None = None,
        *,
        date_range: DateDelta | None = None,
        disabled: bool = False,
        validator: DateValidator | None = None,
        tooltip: str | None = None,
    ) -> None:
        super().__init__(
            value=date,
            name=name,
            id=id,
            classes=classes,
            disabled=disabled,
            tooltip=tooltip,
        )
        self._date_range = date_range
        self.validator = validator

    def _validate_date(self, date: Date | None) -> Date | None:
        if self.validator is None:
            return date

        return self.validator(date)

    def check_action(
        self, action: str, parameters: tuple[object, ...]
    ) -> bool | None:
        if action == "target_today":
            return self.date != Date.today_in_system_tz()
        return True

    def compose(self) -> ComposeResult:
        with Horizontal(id="input-control"):
            yield DateInput(id="date-input").data_bind(date=DatePicker.date)

            yield TargetButton(
                id="target-default",
                disabled=self.date == Date.today_in_system_tz(),
                tooltip="Set the date to today.",
            )
            yield self._compose_expand_button()

        yield (
            DateOverlay(date_range=self._date_range).data_bind(
                date=DatePicker.date, show=DatePicker.expanded
            )
        )

    def _on_date_select_date_changed(
        self,
        message: DateSelect.DateChanged,
    ) -> None:
        message.stop()
        self.date = message.date

    def _watch_date(self, new: Date) -> None:
        self.query_exactly_one("#target-default", Button).disabled = (
            new == Date.today_in_system_tz()
        )
        self.post_message(self.DateChanged(self, new))

    @on(DateInput.DateChanged)
    def _input_updated(self, message: DateInput.DateChanged) -> None:
        message.stop()
        with message.widget.prevent(DateInput.DateChanged):
            self.date = message.date

    def action_clear(self) -> None:
        """Clear the date."""
        self.date = None

    def to_default(self) -> None:
        """Reset the date to today."""
        self.overlay.date_select.scope = DateScope.MONTH
        self.date = Date.today_in_system_tz()

DEFAULT_CSS class-attribute

AbstractPicker {
    layers: base dialog;
    layout: vertical;
    height: 3;
    width: auto;

    &.mini {
        max-height: 1;
        & > #input-control {
            border: none;
            height: 1;
            padding: 0;

            &:blur {
                padding: 0;
            }
            &:focus-within {
                padding: 0;
                border: none;
            }
            Button, AbstractInput {
                border: none;
                padding: 0;
                height: 1;

                &:focus {
                    color: $accent;
                    text-style: none;
                }
                &:disabled {
                    opacity: 50%;
                    text-style: italic;
                }
            }
        }
    }

    & > #input-control {
        background: $surface;
        width: auto;

        &:blur {
            padding: 1;
        }
        &:focus-within {
            border: tall $primary;
            padding: 0;
        }

        Button, AbstractInput {
            border: none;
            padding: 0;
            height: 1;

            &:focus {
                color: $accent;
                text-style: none;
            }
        }
        & > TargetButton {
            min-width: 1;
            max-width: 3;
        }

        & > AbstractInput {
            padding: 0 2;
            &.-invalid {
                color: $error;
                text-style: italic;
            }
            &:focus {
                tint: $primary 2%;
            }
        }
    }
    & > BaseOverlay {
        border: round $secondary;
        overlay: screen !important;
        constrain: inside;
        position: absolute;
        height: auto;
        width: auto;
        background: $surface;
        box-sizing: content-box;
        opacity: 0;

        &:focus,
        &:focus-within {
            border: round $primary;
        }

        & > BaseOverlayWidget {
            width: 40;
            height: auto;
        }
    }
}

BINDINGS class-attribute

BINDINGS: list[BindingType] = [
    Binding(
        "ctrl+shift+d",
        "clear",
        "Clear Value",
        tooltip="Clear the current value.",
    ),
    Binding(
        "ctrl+t",
        "target_default",
        "To Default Value",
        tooltip="Reset to the default value.",
    ),
]

All bindings for BasePicker classes.

Key(s) Description
ctrl+shift+d Clear the current value.
ctrl+t Reset to the default value.

DateValidator class-attribute instance-attribute

DateValidator: TypeAlias = Callable[[Date | None], Date | None]

Callable type for validating date types.

date class-attribute instance-attribute

date = var[Date | None](None, init=False)

Current date for the picker. This is bound to every other subwidget.

DateChanged dataclass

Bases: BaseMessage

Message sent when the date changed.

Source code in src/textual_timepiece/pickers/_date_picker.py
992
993
994
995
996
997
@dataclass
class DateChanged(BaseMessage):
    """Message sent when the date changed."""

    widget: DatePicker
    date: Date | None

action_clear

action_clear() -> None

Clear the date.

Source code in src/textual_timepiece/pickers/_date_picker.py
1080
1081
1082
def action_clear(self) -> None:
    """Clear the date."""
    self.date = None

to_default

to_default() -> None

Reset the date to today.

Source code in src/textual_timepiece/pickers/_date_picker.py
1084
1085
1086
1087
def to_default(self) -> None:
    """Reset the date to today."""
    self.overlay.date_select.scope = DateScope.MONTH
    self.date = Date.today_in_system_tz()

DurationPicker

DurationPickerApp ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ 00:00:00 ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ──────────────────────────────────────── HoursMinutesSeconds +1+4+15+30+15+30 -1-4-15-30-15-30 ────────────────────────────────────────

textual_timepiece.pickers.DurationPicker

Bases: BasePicker[DurationInput, TimeDelta, DurationOverlay]

Picker widget for picking durations.

Duration is limited 99 hours, 59 minutes and 59 seconds.

PARAMETER DESCRIPTION
value

Initial duration value for the widget.

TYPE: T | None DEFAULT: None

name

Name for the widget.

TYPE: str | None DEFAULT: None

id

DOM identifier for the widget.

TYPE: str | None DEFAULT: None

classes

CSS classes for the widget

TYPE: str | None DEFAULT: None

disabled

Whether to disable the widget.

TYPE: bool DEFAULT: False

tooltip

Tooltip to show on hover.

TYPE: str | None DEFAULT: None

CLASS DESCRIPTION
DurationChanged

Message sent when the duration changes.

METHOD DESCRIPTION
to_default

Reset the duration to 00:00:00.

ATTRIBUTE DESCRIPTION
DEFAULT_CSS

TYPE: str

BINDINGS

All bindings for BasePicker classes.

TYPE: list[BindingType]

duration

Current duration. Bound to all the child widgets.

Source code in src/textual_timepiece/pickers/_time_picker.py
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
class DurationPicker(BasePicker[DurationInput, TimeDelta, DurationOverlay]):
    """Picker widget for picking durations.

    Duration is limited 99 hours, 59 minutes and 59 seconds.

    Params:
        value: Initial duration value for the widget.
        name: Name for the widget.
        id: DOM identifier for the widget.
        classes: CSS classes for the widget
        disabled: Whether to disable the widget.
        tooltip: Tooltip to show on hover.
    """

    @dataclass
    class DurationChanged(BaseMessage):
        """Message sent when the duration changes."""

        widget: DurationPicker
        duration: TimeDelta | None

    INPUT = DurationInput
    ALIAS = "duration"

    duration = var[TimeDelta | None](None, init=False)
    """Current duration. Bound to all the child widgets."""

    def compose(self) -> ComposeResult:
        with Horizontal(id="input-control"):
            yield (
                DurationInput(id="data-input").data_bind(
                    duration=DurationPicker.duration
                )
            )
            yield TargetButton(
                id="target-default",
                tooltip="Set the duration to zero.",
            )
            yield self._compose_expand_button()

        yield DurationOverlay().data_bind(show=DurationPicker.expanded)

    def _on_mount(self, event: Mount) -> None:
        self.query_exactly_one("#target-default", Button).disabled = (
            self.duration is None or self.duration.in_seconds() == 0
        )

    def _watch_duration(self, delta: TimeDelta) -> None:
        self.query_exactly_one("#target-default", Button).disabled = (
            delta is None or delta.in_seconds() == 0
        )
        self.post_message(self.DurationChanged(self, delta))

    @on(DurationSelect.DurationRounded)
    def _round_duration(self, message: DurationSelect.DurationRounded) -> None:
        message.stop()
        if self.duration is None:
            return
        seconds = (
            round(self.duration.in_seconds() / message.value) * message.value
        )
        self.duration = TimeDelta(seconds=seconds)

    @on(DurationSelect.DurationAdjusted)
    def _adjust_duration(
        self,
        message: DurationSelect.DurationAdjusted,
    ) -> None:
        message.stop()
        if message.delta is None:
            self.duration = None
        elif self.duration is None:
            self.duration = message.delta
        else:
            self.duration += message.delta

    @on(DurationInput.DurationChanged)
    def _set_duration(self, message: DurationInput.DurationChanged) -> None:
        message.stop()
        with message.control.prevent(DurationInput.DurationChanged):
            self.duration = message.duration

    def to_default(self) -> None:
        """Reset the duration to 00:00:00."""
        self.duration = TimeDelta()

DEFAULT_CSS class-attribute

AbstractPicker {
    layers: base dialog;
    layout: vertical;
    height: 3;
    width: auto;

    &.mini {
        max-height: 1;
        & > #input-control {
            border: none;
            height: 1;
            padding: 0;

            &:blur {
                padding: 0;
            }
            &:focus-within {
                padding: 0;
                border: none;
            }
            Button, AbstractInput {
                border: none;
                padding: 0;
                height: 1;

                &:focus {
                    color: $accent;
                    text-style: none;
                }
                &:disabled {
                    opacity: 50%;
                    text-style: italic;
                }
            }
        }
    }

    & > #input-control {
        background: $surface;
        width: auto;

        &:blur {
            padding: 1;
        }
        &:focus-within {
            border: tall $primary;
            padding: 0;
        }

        Button, AbstractInput {
            border: none;
            padding: 0;
            height: 1;

            &:focus {
                color: $accent;
                text-style: none;
            }
        }
        & > TargetButton {
            min-width: 1;
            max-width: 3;
        }

        & > AbstractInput {
            padding: 0 2;
            &.-invalid {
                color: $error;
                text-style: italic;
            }
            &:focus {
                tint: $primary 2%;
            }
        }
    }
    & > BaseOverlay {
        border: round $secondary;
        overlay: screen !important;
        constrain: inside;
        position: absolute;
        height: auto;
        width: auto;
        background: $surface;
        box-sizing: content-box;
        opacity: 0;

        &:focus,
        &:focus-within {
            border: round $primary;
        }

        & > BaseOverlayWidget {
            width: 40;
            height: auto;
        }
    }
}

BINDINGS class-attribute

BINDINGS: list[BindingType] = [
    Binding(
        "ctrl+shift+d",
        "clear",
        "Clear Value",
        tooltip="Clear the current value.",
    ),
    Binding(
        "ctrl+t",
        "target_default",
        "To Default Value",
        tooltip="Reset to the default value.",
    ),
]

All bindings for BasePicker classes.

Key(s) Description
ctrl+shift+d Clear the current value.
ctrl+t Reset to the default value.

duration class-attribute instance-attribute

duration = var[TimeDelta | None](None, init=False)

Current duration. Bound to all the child widgets.

DurationChanged dataclass

Bases: BaseMessage

Message sent when the duration changes.

Source code in src/textual_timepiece/pickers/_time_picker.py
402
403
404
405
406
407
@dataclass
class DurationChanged(BaseMessage):
    """Message sent when the duration changes."""

    widget: DurationPicker
    duration: TimeDelta | None

to_default

to_default() -> None

Reset the duration to 00:00:00.

Source code in src/textual_timepiece/pickers/_time_picker.py
470
471
472
def to_default(self) -> None:
    """Reset the duration to 00:00:00."""
    self.duration = TimeDelta()

TimePicker

TimePickerApp ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ 12:00:00 ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ──────────────────────────────────────── HoursMinutesSeconds +1+4+15+30+15+30 -1-4-15-30-15-30 ──────────────────────────────────────── 00:0000:3001:0001:30 02:0002:3003:0003:30 04:0004:3005:0005:30 06:0006:3007:0007:30 08:0008:3009:0009:30 10:0010:3011:0011:30 12:0012:3013:0013:30 14:0014:3015:0015:30 16:0016:3017:0017:30 18:0018:3019:0019:30 20:0020:3021:0021:30 22:0022:3023:0023:30 ────────────────────────────────────────

textual_timepiece.pickers.TimePicker

Bases: BasePicker[TimeInput, Time, TimeOverlay]

Time picker for a 24 hour clock.

PARAMETER DESCRIPTION
value

Initial time for the widget.

TYPE: T | None DEFAULT: None

name

Name for the widget.

TYPE: str | None DEFAULT: None

id

DOM identifier for the widget.

TYPE: str | None DEFAULT: None

classes

CSS classes for the widget

TYPE: str | None DEFAULT: None

disabled

Whether to disable the widget.

TYPE: bool DEFAULT: False

tooltip

Tooltip to show on hover.

TYPE: str | None DEFAULT: None

CLASS DESCRIPTION
TimeChanged

Sent when the time is changed with the overlay or other means.

METHOD DESCRIPTION
to_default

Reset time to the local current time.

ATTRIBUTE DESCRIPTION
DEFAULT_CSS

TYPE: str

BINDINGS

All bindings for BasePicker classes.

TYPE: list[BindingType]

time

Currently set time that is bound to the subwidgets. None if empty.

Source code in src/textual_timepiece/pickers/_time_picker.py
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
class TimePicker(BasePicker[TimeInput, Time, TimeOverlay]):
    """Time picker for a 24 hour clock.

    Params:
        value: Initial time for the widget.
        name: Name for the widget.
        id: DOM identifier for the widget.
        classes: CSS classes for the widget
        disabled: Whether to disable the widget.
        tooltip: Tooltip to show on hover.
    """

    @dataclass
    class TimeChanged(BaseMessage):
        """Sent when the time is changed with the overlay or other means."""

        widget: TimePicker
        new_time: Time

    INPUT = TimeInput
    ALIAS = "time"

    time = var[Time | None](None, init=False)
    """Currently set time that is bound to the subwidgets. None if empty."""

    def compose(self) -> ComposeResult:
        with Horizontal(id="input-control"):
            yield TimeInput(id="data-input").data_bind(time=TimePicker.time)
            yield TargetButton(id="target-default", tooltip="Set time to now.")
            yield self._compose_expand_button()

        yield TimeOverlay().data_bind(show=TimePicker.expanded)

    @on(DurationSelect.DurationRounded)
    def _round_duration(self, message: DurationSelect.DurationRounded) -> None:
        message.stop()
        if self.time is None:
            return
        self.time = round_time(self.time, message.value)

    @on(DurationSelect.DurationAdjusted)
    def _adjust_duration(
        self, message: DurationSelect.DurationAdjusted
    ) -> None:
        message.stop()
        self.time = add_time(self.time or Time(), message.delta)

    @on(TimeSelect.TimeSelected)
    def _select_time(self, message: TimeSelect.TimeSelected) -> None:
        message.stop()
        self.time = message.target

    @on(TimeInput.TimeChanged)
    def _change_time(self, message: TimeInput.TimeChanged) -> None:
        message.stop()
        with message.control.prevent(TimeInput.TimeChanged):
            self.time = message.new_time

    def to_default(self) -> None:
        """Reset time to the local current time."""
        self.time = SystemDateTime.now().time()

DEFAULT_CSS class-attribute

AbstractPicker {
    layers: base dialog;
    layout: vertical;
    height: 3;
    width: auto;

    &.mini {
        max-height: 1;
        & > #input-control {
            border: none;
            height: 1;
            padding: 0;

            &:blur {
                padding: 0;
            }
            &:focus-within {
                padding: 0;
                border: none;
            }
            Button, AbstractInput {
                border: none;
                padding: 0;
                height: 1;

                &:focus {
                    color: $accent;
                    text-style: none;
                }
                &:disabled {
                    opacity: 50%;
                    text-style: italic;
                }
            }
        }
    }

    & > #input-control {
        background: $surface;
        width: auto;

        &:blur {
            padding: 1;
        }
        &:focus-within {
            border: tall $primary;
            padding: 0;
        }

        Button, AbstractInput {
            border: none;
            padding: 0;
            height: 1;

            &:focus {
                color: $accent;
                text-style: none;
            }
        }
        & > TargetButton {
            min-width: 1;
            max-width: 3;
        }

        & > AbstractInput {
            padding: 0 2;
            &.-invalid {
                color: $error;
                text-style: italic;
            }
            &:focus {
                tint: $primary 2%;
            }
        }
    }
    & > BaseOverlay {
        border: round $secondary;
        overlay: screen !important;
        constrain: inside;
        position: absolute;
        height: auto;
        width: auto;
        background: $surface;
        box-sizing: content-box;
        opacity: 0;

        &:focus,
        &:focus-within {
            border: round $primary;
        }

        & > BaseOverlayWidget {
            width: 40;
            height: auto;
        }
    }
}

BINDINGS class-attribute

BINDINGS: list[BindingType] = [
    Binding(
        "ctrl+shift+d",
        "clear",
        "Clear Value",
        tooltip="Clear the current value.",
    ),
    Binding(
        "ctrl+t",
        "target_default",
        "To Default Value",
        tooltip="Reset to the default value.",
    ),
]

All bindings for BasePicker classes.

Key(s) Description
ctrl+shift+d Clear the current value.
ctrl+t Reset to the default value.

time class-attribute instance-attribute

time = var[Time | None](None, init=False)

Currently set time that is bound to the subwidgets. None if empty.

TimeChanged dataclass

Bases: BaseMessage

Sent when the time is changed with the overlay or other means.

Source code in src/textual_timepiece/pickers/_time_picker.py
595
596
597
598
599
600
@dataclass
class TimeChanged(BaseMessage):
    """Sent when the time is changed with the overlay or other means."""

    widget: TimePicker
    new_time: Time

to_default

to_default() -> None

Reset time to the local current time.

Source code in src/textual_timepiece/pickers/_time_picker.py
641
642
643
def to_default(self) -> None:
    """Reset time to the local current time."""
    self.time = SystemDateTime.now().time()

DateTimePicker

DateTimePickerApp ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔    -  -     :  :   ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ──────────────────────────────────────────────────────────────────────────── HoursMinutesSeconds March 2025+1+4+15+30+15+30 -1-4-15-30-15-30 MonTueWedThuFriSatSun00:0000:3001:0001:30 02:0002:3003:0003:30   1  204:0004:3005:0005:30 06:0006:3007:0007:30   3  4  5  6  7  8  908:0008:3009:0009:30 10:0010:3011:0011:30  10 11 12 13 14 15 1612:0012:3013:0013:30 14:0014:3015:0015:30  17 18 19 20 21 22 2316:0016:3017:0017:30 18:0018:3019:0019:30  24 25 26 27 28 29 3020:0020:3021:0021:30 22:0022:3023:0023:30  31 ────────────────────────────────────────────────────────────────────────────

textual_timepiece.pickers.DateTimePicker

Bases: BasePicker[DateTimeInput, LocalDateTime, DateTimeOverlay]

Datetime picker with a date and time in one input.

PARAMETER DESCRIPTION
value

Initial datetime value for the widget.

TYPE: T | None DEFAULT: None

name

Name for the widget.

TYPE: str | None DEFAULT: None

id

DOM identifier for the widget.

TYPE: str | None DEFAULT: None

classes

CSS classes for the widget

TYPE: str | None DEFAULT: None

disabled

Whether to disable the widget.

TYPE: bool DEFAULT: False

tooltip

Tooltip to show on hover.

TYPE: str | None DEFAULT: None

CLASS DESCRIPTION
DateTimeChanged

Message sent when the datetime is updated.

METHOD DESCRIPTION
to_default

Reset the picker datetime to the current time.

ATTRIBUTE DESCRIPTION
DEFAULT_CSS

TYPE: str

BINDINGS

All bindings for BasePicker classes.

TYPE: list[BindingType]

datetime

The current set datetime. Bound of to all subwidgets.

date

Computed date based on the datetime for the overlay.

Source code in src/textual_timepiece/pickers/_datetime_picker.py
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
class DateTimePicker(
    BasePicker[DateTimeInput, LocalDateTime, DateTimeOverlay]
):
    """Datetime picker with a date and time in one input.


    Params:
        value: Initial datetime value for the widget.
        name: Name for the widget.
        id: DOM identifier for the widget.
        classes: CSS classes for the widget
        disabled: Whether to disable the widget.
        tooltip: Tooltip to show on hover.
    """

    @dataclass
    class DateTimeChanged(BaseMessage):
        """Message sent when the datetime is updated."""

        widget: DateTimePicker
        datetime: LocalDateTime | None

    INPUT = DateTimeInput
    ALIAS = "datetime"

    datetime = var[LocalDateTime | None](None, init=False)
    """The current set datetime. Bound of to all subwidgets."""
    date = var[Date | None](None, init=False)
    """Computed date based on the datetime for the overlay."""

    def compose(self) -> ComposeResult:
        with Horizontal(id="input-control"):
            yield DateTimeInput().data_bind(DateTimePicker.datetime)
            yield TargetButton(
                id="target-default",
                tooltip="Set the datetime to now.",
            )
            yield self._compose_expand_button()

        yield (
            DateTimeOverlay().data_bind(
                date=DateTimePicker.date,
                show=DateTimePicker.expanded,
            )
        )

    def _compute_date(self) -> Date | None:
        if self.datetime:
            return self.datetime.date()
        return None

    def _watch_datetime(self, datetime: LocalDateTime | None) -> None:
        self.post_message(self.DateTimeChanged(self, datetime))

    def _on_date_select_date_changed(
        self,
        message: DateSelect.DateChanged,
    ) -> None:
        message.stop()
        if not message.date:
            return
        if self.datetime:
            self.datetime = self.datetime.time().on(message.date)
        else:
            self.datetime = message.date.at(Time())

    @on(DurationSelect.DurationRounded)
    def _round_time(self, message: DurationSelect.DurationRounded) -> None:
        message.stop()
        if self.datetime is None:
            return

        time = round_time(self.datetime.time(), message.value)
        self.datetime = self.datetime.replace_time(time)

    @on(DurationSelect.DurationAdjusted)
    def _adjust_time(self, message: DurationSelect.DurationAdjusted) -> None:
        message.stop()
        if self.datetime:
            self.datetime = self.datetime.add(message.delta, ignore_dst=True)
        else:
            self.datetime = SystemDateTime.now().local()

    @on(TimeSelect.TimeSelected)
    def _set_time(self, message: TimeSelect.TimeSelected) -> None:
        message.stop()
        if self.datetime is None:
            self.datetime = (
                SystemDateTime.now().local().replace_time(message.target)
            )
        else:
            self.datetime = self.datetime.replace_time(message.target)

    @on(DateTimeInput.DateTimeChanged)
    def _dt_input_changed(
        self, message: DateTimeInput.DateTimeChanged
    ) -> None:
        message.stop()
        with self.input_widget.prevent(DateTimeInput.DateTimeChanged):
            self.datetime = message.datetime

    def to_default(self) -> None:
        """Reset the picker datetime to the current time."""
        self.datetime = SystemDateTime.now().local()
        self.overlay.date_select.scope = DateScope.MONTH

DEFAULT_CSS class-attribute

AbstractPicker {
    layers: base dialog;
    layout: vertical;
    height: 3;
    width: auto;

    &.mini {
        max-height: 1;
        & > #input-control {
            border: none;
            height: 1;
            padding: 0;

            &:blur {
                padding: 0;
            }
            &:focus-within {
                padding: 0;
                border: none;
            }
            Button, AbstractInput {
                border: none;
                padding: 0;
                height: 1;

                &:focus {
                    color: $accent;
                    text-style: none;
                }
                &:disabled {
                    opacity: 50%;
                    text-style: italic;
                }
            }
        }
    }

    & > #input-control {
        background: $surface;
        width: auto;

        &:blur {
            padding: 1;
        }
        &:focus-within {
            border: tall $primary;
            padding: 0;
        }

        Button, AbstractInput {
            border: none;
            padding: 0;
            height: 1;

            &:focus {
                color: $accent;
                text-style: none;
            }
        }
        & > TargetButton {
            min-width: 1;
            max-width: 3;
        }

        & > AbstractInput {
            padding: 0 2;
            &.-invalid {
                color: $error;
                text-style: italic;
            }
            &:focus {
                tint: $primary 2%;
            }
        }
    }
    & > BaseOverlay {
        border: round $secondary;
        overlay: screen !important;
        constrain: inside;
        position: absolute;
        height: auto;
        width: auto;
        background: $surface;
        box-sizing: content-box;
        opacity: 0;

        &:focus,
        &:focus-within {
            border: round $primary;
        }

        & > BaseOverlayWidget {
            width: 40;
            height: auto;
        }
    }
}

BINDINGS class-attribute

BINDINGS: list[BindingType] = [
    Binding(
        "ctrl+shift+d",
        "clear",
        "Clear Value",
        tooltip="Clear the current value.",
    ),
    Binding(
        "ctrl+t",
        "target_default",
        "To Default Value",
        tooltip="Reset to the default value.",
    ),
]

All bindings for BasePicker classes.

Key(s) Description
ctrl+shift+d Clear the current value.
ctrl+t Reset to the default value.

datetime class-attribute instance-attribute

datetime = var[LocalDateTime | None](None, init=False)

The current set datetime. Bound of to all subwidgets.

date class-attribute instance-attribute

date = var[Date | None](None, init=False)

Computed date based on the datetime for the overlay.

DateTimeChanged dataclass

Bases: BaseMessage

Message sent when the datetime is updated.

Source code in src/textual_timepiece/pickers/_datetime_picker.py
227
228
229
230
231
232
@dataclass
class DateTimeChanged(BaseMessage):
    """Message sent when the datetime is updated."""

    widget: DateTimePicker
    datetime: LocalDateTime | None

to_default

to_default() -> None

Reset the picker datetime to the current time.

Source code in src/textual_timepiece/pickers/_datetime_picker.py
313
314
315
316
def to_default(self) -> None:
    """Reset the picker datetime to the current time."""
    self.datetime = SystemDateTime.now().local()
    self.overlay.date_select.scope = DateScope.MONTH

DateRangePicker

DateRangePickerApp ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ 2025-03-10🔓    -  -   ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ─────────────────────────────────────────────────────────────────────────────── > March 2025>March 2025 > MonTueWedThuFriSatSun>MonTueWedThuFriSatSun >   1  2>  1  2 >   3  4  5  6  7  8  9>  3  4  5  6  7  8  9 >  10 11 12 13 14 15 16> 10 11 12 13 14 15 16 >  17 18 19 20 21 22 23> 17 18 19 20 21 22 23 >  24 25 26 27 28 29 30> 24 25 26 27 28 29 30 >  31> 31 > ───────────────────────────────────────────────────────────────────────────────

textual_timepiece.pickers.DateRangePicker

Bases: AbstractPicker[DateRangeOverlay]

Date range picker for picking inclusive date ranges.

PARAMETER DESCRIPTION
start

Initial start date for the picker.

TYPE: Date | None DEFAULT: None

end

Initial end date for the picker.

TYPE: Date | None DEFAULT: None

name

Name for the widget.

TYPE: str | None DEFAULT: None

id

DOM identifier for the widget.

TYPE: str | None DEFAULT: None

classes

CSS classes for the widget

TYPE: str | None DEFAULT: None

date_range

Date range to restrict the date to. If provided the picker lock will be permanently on for the widgets lifetime or when re-enabled programmatically.

TYPE: DateDelta | None DEFAULT: None

disabled

Whether to disable the widget

TYPE: bool DEFAULT: False

tooltip

Tooltip to show on hover.

TYPE: str | None DEFAULT: None

Examples:

    def compose(self) -> ComposeResult:
        yield DateRangePicker(Date(2025, 2, 1), Date(2025, 3, 1))
    def compose(self) -> ComposeResult:
        yield DateRangePicker(Date.today_in_system_tz()).disable_end()

    def action_stop(self) -> None:
        pick = self.query_one(DateRangePicker)
        pick.disable_end(disable=False)
        pick.end_date = Date.today_in_system_tz()
CLASS DESCRIPTION
DateRangeChanged

Message sent when the date range has changed.

METHOD DESCRIPTION
action_clear

Clear the start and end dates.

disable_start

Utility method to disable start input widgets.

disable_end

Utility method to disable end input widgets.

ATTRIBUTE DESCRIPTION
DEFAULT_CSS

TYPE: str

BINDINGS

All bindings for DateTimeRangePicker.

TYPE: list[BindingType]

start_date

Picker start date. Bound to sub widgets.

end_date

Picker end date. Bound to sub widgets.

Source code in src/textual_timepiece/pickers/_timerange_picker.py
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
class DateRangePicker(AbstractPicker[DateRangeOverlay]):
    """Date range picker for picking inclusive date ranges.

    Params:
        start: Initial start date for the picker.
        end: Initial end date for the picker.
        name: Name for the widget.
        id: DOM identifier for the widget.
        classes: CSS classes for the widget
        date_range: Date range to restrict the date to. If provided the picker
            lock will be permanently on for the widgets lifetime or when
            re-enabled programmatically.
        disabled: Whether to disable the widget
        tooltip: Tooltip to show on hover.

    Examples:
        ```python
            def compose(self) -> ComposeResult:
                yield DateRangePicker(Date(2025, 2, 1), Date(2025, 3, 1))
        ```

        ```python
            def compose(self) -> ComposeResult:
                yield DateRangePicker(Date.today_in_system_tz()).disable_end()

            def action_stop(self) -> None:
                pick = self.query_one(DateRangePicker)
                pick.disable_end(disable=False)
                pick.end_date = Date.today_in_system_tz()
        ```
    """

    @dataclass
    class DateRangeChanged(BaseMessage):
        """Message sent when the date range has changed."""

        widget: DateRangePicker
        start: Date | None
        end: Date | None

    BINDING_GROUP_TITLE = "Date Range Picker"

    BINDINGS: ClassVar[list[BindingType]] = [
        Binding(
            "ctrl+shift+d",
            "clear",
            "Clear Dates",
            tooltip="Clear both the start and end date.",
        ),
        Binding(
            "ctrl+t",
            "target_default_start",
            "Start To Today",
            tooltip="Set the start date to todays date.",
        ),
        Binding(
            "alt+ctrl+t",
            "target_default_end",
            "End To Today",
            tooltip="Set the end date to today or the start date.",
        ),
    ]
    """All bindings for `DateTimeRangePicker`.

    | Key(s) | Description |
    | :- | :- |
    | ctrl+shift+d | Clear end and start datetime. |
    | ctrl+t | Set the start date to todays date. |
    | alt+ctrl+t | Set the end date to today or the start date. |
    """

    start_date = var[Date | None](None, init=False)
    """Picker start date. Bound to sub widgets."""
    end_date = var[Date | None](None, init=False)
    """Picker end date. Bound to sub widgets."""

    def __init__(
        self,
        start: Date | None = None,
        end: Date | None = None,
        name: str | None = None,
        id: str | None = None,
        classes: str | None = None,
        *,
        date_range: DateDelta | None = None,
        disabled: bool = False,
        tooltip: str | None = None,
    ) -> None:
        super().__init__(name, id, classes, disabled=disabled, tooltip=tooltip)
        self.set_reactive(DateRangePicker.start_date, start)
        self.set_reactive(DateRangePicker.end_date, end)
        self._date_range = date_range

    def compose(self) -> ComposeResult:
        with Horizontal(id="input-control"):
            yield DateInput(id="start-date-input").data_bind(
                date=DateRangePicker.start_date,
            )

            yield TargetButton(
                id="target-default-start",
                tooltip="Set the start date to today.",
            )
            yield LockButton(
                is_locked=self._date_range is not None,
                id="lock-button",
                tooltip="Lock the range inbetween the dates.",
                disabled=self._date_range is not None,
            )

            yield DateInput(id="stop-date-input").data_bind(
                date=DateRangePicker.end_date,
            )
            yield TargetButton(
                id="target-default-end",
                tooltip="Set the end date to today or the start date.",
            )
            yield self._compose_expand_button()

        yield DateRangeOverlay().data_bind(
            show=DateRangePicker.expanded,
            start=DateRangePicker.start_date,
            stop=DateRangePicker.end_date,
        )

    def _watch_start_date(self, date: Date | None) -> None:
        if date and self._date_range:
            with self.prevent(self.DateRangeChanged):
                self.end_date = date + self._date_range
        self.post_message(self.DateRangeChanged(self, date, self.end_date))

    def _watch_end_date(self, date: Date | None) -> None:
        if date and self._date_range:
            with self.prevent(self.DateRangeChanged):
                self.start_date = date - self._date_range
        self.post_message(self.DateRangeChanged(self, self.start_date, date))

    @on(DateSelect.DateChanged)
    @on(DateSelect.EndDateChanged)
    def _dialog_date_changed(
        self,
        message: DateSelect.DateChanged | DateSelect.EndDateChanged,
    ) -> None:
        """Handles changes in dates including, keeping dates the same span."""
        message.stop()
        if isinstance(message, DateSelect.DateChanged):
            self.start_date = message.date
        else:
            self.end_date = message.date

    @on(DateInput.DateChanged, "#start-date-input")
    @on(DateInput.DateChanged, "#stop-date-input")
    def _date_input_change(self, message: DateInput.DateChanged) -> None:
        message.stop()
        with message.control.prevent(DateInput.DateChanged):
            if message.control.id == "start-date-input":
                self.start_date = message.date
            else:
                self.end_date = message.date

    @on(Button.Pressed, "#target-default-start")
    def _action_target_default_start(
        self,
        message: Button.Pressed | None = None,
    ) -> None:
        if message:
            message.stop()
        new_date = Date.today_in_system_tz()
        if not self.end_date or new_date <= self.end_date:
            self.start_date = new_date
        else:
            self.start_date = self.end_date

    @on(Button.Pressed, "#target-default-end")
    def _action_target_default_end(
        self,
        message: Button.Pressed | None = None,
    ) -> None:
        if message:
            message.stop()
        new_date = Date.today_in_system_tz()
        if not self.start_date or (new_date) >= self.start_date:
            self.end_date = new_date
        else:
            self.end_date = self.start_date

    @on(Button.Pressed, "#lock-button")
    def _lock_delta(self, message: Button.Pressed) -> None:
        message.stop()

        if (
            self.end_date
            and self.start_date
            and cast(LockButton, message.control).locked
        ):
            self._date_range = self.end_date - self.start_date
        else:
            self._date_range = None
            cast(LockButton, message.control).locked = False

    def action_clear(self) -> None:
        """Clear the start and end dates."""
        self.start_date = None
        self.end_date = None

    def disable_start(self, *, disable: bool = True) -> Self:
        """Utility method to disable start input widgets."""
        self.start_input.disabled = disable
        self.overlay.query_one(
            "#start-date-select", DateSelect
        ).disabled = disable
        self.query_one("#target-default-start", Button).disabled = disable
        return self

    def disable_end(self, *, disable: bool = True) -> Self:
        """Utility method to disable end input widgets."""
        self.end_input.disabled = disable
        self.overlay.query_one(
            "#end-date-select", EndDateSelect
        ).disabled = disable
        self.query_one("#target-default-end", Button).disabled = disable
        return self

    @cached_property
    def start_input(self) -> DateInput:
        return self.query_exactly_one("#start-date-input", DateInput)

    @cached_property
    def end_input(self) -> DateInput:
        return self.query_exactly_one("#stop-date-input", DateInput)

    @cached_property
    def lock_button(self) -> LockButton:
        return cast(LockButton, self.query_exactly_one(LockButton))

DEFAULT_CSS class-attribute

AbstractPicker {
    layers: base dialog;
    layout: vertical;
    height: 3;
    width: auto;

    &.mini {
        max-height: 1;
        & > #input-control {
            border: none;
            height: 1;
            padding: 0;

            &:blur {
                padding: 0;
            }
            &:focus-within {
                padding: 0;
                border: none;
            }
            Button, AbstractInput {
                border: none;
                padding: 0;
                height: 1;

                &:focus {
                    color: $accent;
                    text-style: none;
                }
                &:disabled {
                    opacity: 50%;
                    text-style: italic;
                }
            }
        }
    }

    & > #input-control {
        background: $surface;
        width: auto;

        &:blur {
            padding: 1;
        }
        &:focus-within {
            border: tall $primary;
            padding: 0;
        }

        Button, AbstractInput {
            border: none;
            padding: 0;
            height: 1;

            &:focus {
                color: $accent;
                text-style: none;
            }
        }
        & > TargetButton {
            min-width: 1;
            max-width: 3;
        }

        & > AbstractInput {
            padding: 0 2;
            &.-invalid {
                color: $error;
                text-style: italic;
            }
            &:focus {
                tint: $primary 2%;
            }
        }
    }
    & > BaseOverlay {
        border: round $secondary;
        overlay: screen !important;
        constrain: inside;
        position: absolute;
        height: auto;
        width: auto;
        background: $surface;
        box-sizing: content-box;
        opacity: 0;

        &:focus,
        &:focus-within {
            border: round $primary;
        }

        & > BaseOverlayWidget {
            width: 40;
            height: auto;
        }
    }
}

BINDINGS class-attribute

BINDINGS: list[BindingType] = [
    Binding(
        "ctrl+shift+d",
        "clear",
        "Clear Dates",
        tooltip="Clear both the start and end date.",
    ),
    Binding(
        "ctrl+t",
        "target_default_start",
        "Start To Today",
        tooltip="Set the start date to todays date.",
    ),
    Binding(
        "alt+ctrl+t",
        "target_default_end",
        "End To Today",
        tooltip="Set the end date to today or the start date.",
    ),
]

All bindings for DateTimeRangePicker.

Key(s) Description
ctrl+shift+d Clear end and start datetime.
ctrl+t Set the start date to todays date.
alt+ctrl+t Set the end date to today or the start date.

start_date class-attribute instance-attribute

start_date = var[Date | None](None, init=False)

Picker start date. Bound to sub widgets.

end_date class-attribute instance-attribute

end_date = var[Date | None](None, init=False)

Picker end date. Bound to sub widgets.

DateRangeChanged dataclass

Bases: BaseMessage

Message sent when the date range has changed.

Source code in src/textual_timepiece/pickers/_timerange_picker.py
102
103
104
105
106
107
108
@dataclass
class DateRangeChanged(BaseMessage):
    """Message sent when the date range has changed."""

    widget: DateRangePicker
    start: Date | None
    end: Date | None

action_clear

action_clear() -> None

Clear the start and end dates.

Source code in src/textual_timepiece/pickers/_timerange_picker.py
270
271
272
273
def action_clear(self) -> None:
    """Clear the start and end dates."""
    self.start_date = None
    self.end_date = None

disable_start

disable_start(*, disable: bool = True) -> Self

Utility method to disable start input widgets.

Source code in src/textual_timepiece/pickers/_timerange_picker.py
275
276
277
278
279
280
281
282
def disable_start(self, *, disable: bool = True) -> Self:
    """Utility method to disable start input widgets."""
    self.start_input.disabled = disable
    self.overlay.query_one(
        "#start-date-select", DateSelect
    ).disabled = disable
    self.query_one("#target-default-start", Button).disabled = disable
    return self

disable_end

disable_end(*, disable: bool = True) -> Self

Utility method to disable end input widgets.

Source code in src/textual_timepiece/pickers/_timerange_picker.py
284
285
286
287
288
289
290
291
def disable_end(self, *, disable: bool = True) -> Self:
    """Utility method to disable end input widgets."""
    self.end_input.disabled = disable
    self.overlay.query_one(
        "#end-date-select", EndDateSelect
    ).disabled = disable
    self.query_one("#target-default-end", Button).disabled = disable
    return self

DateTimeRangePicker

DateTimeRangePickerApp ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ 2025-03-1012:49:39🔓2025-03-1102:49:39 ───────────────────────────────────────────────────────────────────────────── HoursMinutesSecondsHoursMinutesSeconds +1+4+15+30+15+30+1+4+15+30+15+30 -1-4-15-30-15-30-1-4-15-30-15-30 March 2025March 2025 MonTueWedThuFriSatSunMonTueWedThuFriSatSun   1  2  1  2   3  4  5  6  7  8  9  3  4  5  6  7  8  9  10 11 12 13 14 15 16 10 11 12 13 14 15 16  17 18 19 20 21 22 23 17 18 19 20 21 22 23  24 25 26 27 28 29 30 24 25 26 27 28 29 30  31 31 ─────────────────────────────────────────────────────────────────────────────

textual_timepiece.pickers.DateTimeRangePicker

Bases: AbstractPicker[DateTimeRangeOverlay]

Datetime range picker with two datetime inputs.

PARAMETER DESCRIPTION
start

Initial start datetime for the picker.

TYPE: LocalDateTime | None DEFAULT: None

end

Initial end datetime for the picker.

TYPE: LocalDateTime | None DEFAULT: None

name

Name for the widget.

TYPE: str | None DEFAULT: None

id

DOM identifier for the widget.

TYPE: str | None DEFAULT: None

classes

CSS classes for the widget

TYPE: str | None DEFAULT: None

time_range

Time range to restrict the datetimes to. If provided the picker lock will be permanently on for the widgets lifetime or re-enabled programmatically.

TYPE: TimeDelta | None DEFAULT: None

disabled

Whether to disable the widget

TYPE: bool DEFAULT: False

tooltip

Tooltip to show on hover.

TYPE: str | None DEFAULT: None

Examples:

    def compose(self) -> ComposeResult:
        now = SystemDateTime.now().local()
        yield DateTimeRangePicker(now, time_range=TimeDelta(hours=5))
    def compose(self) -> ComposeResult:
        yield DateTimeRangePicker(SystemDateTime.now().local())

    def action_stop(self) -> None:
        pick = self.query_one(DateTimeRangePicker)
        pick.end_dt = SystemDateTime.now().local()
CLASS DESCRIPTION
DTRangeChanged

Message sent when the datetime range has changed.

METHOD DESCRIPTION
adjust_start_date

Set or clear the current start date depending on the input.

adjust_end_date

Set or clear the current end date depending on the input.

action_clear

Clear the start and end datetimes.

disable_start

Utility method to disable start input widgets.

disable_end

Utility method to disable end input widgets.

ATTRIBUTE DESCRIPTION
DEFAULT_CSS

TYPE: str

BINDINGS

All bindings for DateTimeRangePicker.

TYPE: list[BindingType]

start_dt

Picker start datetime. Bound to all the parent widgets.

end_dt

Picker end datetime. Bound to all the parent widgets.

start_date

Start date dynamically computed depending on start datetime.

end_date

End date dynamically computed depending on the end datetime.

Source code in src/textual_timepiece/pickers/_timerange_picker.py
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
class DateTimeRangePicker(AbstractPicker[DateTimeRangeOverlay]):
    """Datetime range picker with two datetime inputs.

    Params:
        start: Initial start datetime for the picker.
        end: Initial end datetime for the picker.
        name: Name for the widget.
        id: DOM identifier for the widget.
        classes: CSS classes for the widget
        time_range: Time range to restrict the datetimes to. If provided the
            picker lock will be permanently on for the widgets lifetime or
            re-enabled programmatically.
        disabled: Whether to disable the widget
        tooltip: Tooltip to show on hover.

    Examples:
        ```python
            def compose(self) -> ComposeResult:
                now = SystemDateTime.now().local()
                yield DateTimeRangePicker(now, time_range=TimeDelta(hours=5))
        ```

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

            def action_stop(self) -> None:
                pick = self.query_one(DateTimeRangePicker)
                pick.end_dt = SystemDateTime.now().local()
        ```
    """

    @dataclass
    class DTRangeChanged(BaseMessage):
        """Message sent when the datetime range has changed."""

        widget: DateTimeRangePicker
        start: LocalDateTime | None
        end: LocalDateTime | None

    BINDING_GROUP_TITLE = "Datetime Range Picker"

    BINDINGS: ClassVar[list[BindingType]] = [
        Binding(
            "ctrl+shift+d",
            "clear",
            "Clear",
            tooltip="Clear end and start datetime.",
        ),
        Binding(
            "ctrl+t",
            "target_default_start",
            "Start To Today",
            tooltip="Set the start datetime to now.",
        ),
        Binding(
            "alt+ctrl+t",
            "target_default_end",
            "End To Today",
            tooltip="Set the end datetime to now or the start datetime.",
        ),
    ]
    """All bindings for `DateTimeRangePicker`.

    | Key(s) | Description |
    | :- | :- |
    | ctrl+shift+d | Clear end and start datetime. |
    | ctrl+t | Set the start datetime to now. |
    | alt+ctrl+t | Set the end datetime to now or the start datetime. |
    """

    start_dt = var[LocalDateTime | None](None, init=False)
    """Picker start datetime. Bound to all the parent widgets."""
    end_dt = var[LocalDateTime | None](None, init=False)
    """Picker end datetime. Bound to all the parent widgets."""

    start_date = var[Date | None](None, init=False)
    """Start date dynamically computed depending on start datetime."""
    end_date = var[Date | None](None, init=False)
    """End date dynamically computed depending on the end datetime."""

    def __init__(
        self,
        start: LocalDateTime | None = None,
        end: LocalDateTime | None = None,
        name: str | None = None,
        id: str | None = None,
        classes: str | None = None,
        *,
        time_range: TimeDelta | None = None,
        disabled: bool = False,
        tooltip: str | None = None,
    ) -> None:
        super().__init__(
            name=name,
            id=id,
            classes=classes,
            disabled=disabled,
            tooltip=tooltip,
        )
        self.set_reactive(DateTimeRangePicker.start_dt, start)
        self.set_reactive(DateTimeRangePicker.end_dt, end)

        self._time_range = time_range

    def compose(self) -> ComposeResult:
        with Horizontal(id="input-control"):
            yield DateTimeInput(id="start-dt-input").data_bind(
                datetime=DateTimeRangePicker.start_dt,
            )

            yield TargetButton(
                id="target-default-start",
                tooltip="Set the start time to now.",
            )
            yield LockButton(
                is_locked=self._time_range is not None,
                id="lock-button",
                tooltip="Lock the time range.",
                disabled=self._time_range is not None,
            )

            yield DateTimeInput(id="end-dt-input").data_bind(
                datetime=DateTimeRangePicker.end_dt,
            )
            yield TargetButton(
                id="target-default-end",
                tooltip="Set the end time to now or the start time.",
            )
            yield self._compose_expand_button()

        yield DateTimeRangeOverlay().data_bind(
            show=DateTimeDurationPicker.expanded,
            start=DateTimeDurationPicker.start_date,
            stop=DateTimeDurationPicker.end_date,
        )

    def _compute_start_date(self) -> Date | None:
        if self.start_dt is None:
            return None
        return self.start_dt.date()

    def _compute_end_date(self) -> Date | None:
        if self.end_dt is None:
            return None

        return self.end_dt.date()

    def _watch_start_dt(self, new: LocalDateTime | None) -> None:
        if new and self._time_range:
            with self.prevent(self.DTRangeChanged):
                self.end_dt = new.add(
                    seconds=self._time_range.in_seconds(),
                    ignore_dst=True,
                )
        self.post_message(self.DTRangeChanged(self, new, self.end_dt))

    def _watch_end_dt(self, new: LocalDateTime | None) -> None:
        if new and self._time_range:
            with self.prevent(self.DTRangeChanged):
                self.start_dt = new.subtract(
                    seconds=self._time_range.in_seconds(),
                    ignore_dst=True,
                )
        self.post_message(self.DTRangeChanged(self, self.start_dt, new))

    @on(Button.Pressed, "#lock-button")
    def _lock_delta(self, message: Button.Pressed) -> None:
        message.stop()

        if (
            cast(LockButton, message.control).locked
            and self.end_dt
            and self.start_dt
        ):
            self._time_range = self.end_dt.difference(
                self.start_dt, ignore_dst=True
            )
        else:
            self._time_range = None
            cast(LockButton, message.control).locked = False

    @on(DateSelect.DateChanged)
    @on(DateSelect.EndDateChanged)
    def _dialog_date_changed(self, message: DateSelect.DateChanged) -> None:
        message.stop()
        if isinstance(message, DateSelect.DateChanged):
            self.adjust_start_date(message.date)
        else:
            self.adjust_end_date(message.date)

    def adjust_start_date(self, date: Date | None) -> None:
        """Set or clear the current start date depending on the input."""
        if self.start_dt and date:
            self.start_dt = self.start_dt.replace_date(date)
        elif date:
            self.start_dt = date.at(Time())
        else:
            self.start_dt = date

    def adjust_end_date(self, date: Date | None) -> None:
        """Set or clear the current end date depending on the input."""
        if self.end_dt and date:
            self.end_dt = self.end_dt.replace_date(date)
        elif date:
            self.end_dt = date.at(Time())
        else:
            self.end_dt = date

    def action_clear(self) -> None:
        """Clear the start and end datetimes."""
        self.start_dt = None
        self.end_dt = None

    @on(DurationSelect.DurationRounded)
    def _round_duration(self, message: DurationSelect.DurationRounded) -> None:
        message.stop()
        if message.widget.id == "start-time-select":
            if self.start_dt is None:
                return
            time = round_time(self.start_dt.time(), message.value)
            self.start_dt = self.start_dt.replace_time(time)

        elif self.end_dt:
            time = round_time(self.end_dt.time(), message.value)
            self.end_dt = self.end_dt.replace_time(time)

    @on(DurationSelect.DurationAdjusted)
    def _adjust_duration(
        self, message: DurationSelect.DurationAdjusted
    ) -> None:
        message.stop()
        if message.widget.id == "start-time-select":
            if self.start_dt is None:
                return
            self.start_dt = self.start_dt.add(message.delta, ignore_dst=True)
        elif self.end_dt:
            self.end_dt = self.end_dt.add(message.delta, ignore_dst=True)
        elif self.start_dt:
            self.end_dt = self.start_dt.add(message.delta, ignore_dst=True)

    @on(DateTimeInput.DateTimeChanged, "#start-dt-input")
    def _start_dt_input_changed(
        self,
        message: DateTimeInput.DateTimeChanged,
    ) -> None:
        message.stop()
        with message.control.prevent(DateTimeInput.DateTimeChanged):
            self.start_dt = message.datetime

    @on(DateTimeInput.DateTimeChanged, "#end-dt-input")
    def _end_dt_input_changed(
        self, message: DateTimeInput.DateTimeChanged
    ) -> None:
        message.stop()
        with message.control.prevent(DateTimeInput.DateTimeChanged):
            self.end_dt = message.datetime

    def disable_start(self, *, disable: bool = True) -> Self:
        """Utility method to disable start input widgets."""
        self.start_input.disabled = disable
        self.overlay.query_one("#start-column", Vertical).disabled = disable
        self.query_one("#target-default-start", Button).disabled = disable
        return self

    def disable_end(self, *, disable: bool = True) -> Self:
        """Utility method to disable end input widgets."""
        self.end_input.disabled = disable
        self.overlay.query_one("#end-column", Vertical).disabled = disable
        self.query_one("#target-default-end", Button).disabled = disable
        return self

    @on(Button.Pressed, "#target-default-start")
    def _action_target_default_start(
        self,
        message: Button.Pressed | None = None,
    ) -> None:
        if message:
            message.stop()
        self.start_dt = SystemDateTime.now().local()

    @on(Button.Pressed, "#target-default-end")
    def _action_target_default_end(
        self,
        message: Button.Pressed | None = None,
    ) -> None:
        if message:
            message.stop()
        now = SystemDateTime.now().local()
        if not self.start_dt or now >= self.start_dt:
            self.end_dt = now
        else:
            self.end_dt = self.start_dt

    @cached_property
    def start_input(self) -> DateTimeInput:
        return self.query_exactly_one("#start-dt-input", DateTimeInput)

    @cached_property
    def end_input(self) -> DateTimeInput:
        return self.query_exactly_one("#end-dt-input", DateTimeInput)

    @cached_property
    def lock_button(self) -> LockButton:
        return cast(LockButton, self.query_exactly_one(LockButton))

DEFAULT_CSS class-attribute

AbstractPicker {
    layers: base dialog;
    layout: vertical;
    height: 3;
    width: auto;

    &.mini {
        max-height: 1;
        & > #input-control {
            border: none;
            height: 1;
            padding: 0;

            &:blur {
                padding: 0;
            }
            &:focus-within {
                padding: 0;
                border: none;
            }
            Button, AbstractInput {
                border: none;
                padding: 0;
                height: 1;

                &:focus {
                    color: $accent;
                    text-style: none;
                }
                &:disabled {
                    opacity: 50%;
                    text-style: italic;
                }
            }
        }
    }

    & > #input-control {
        background: $surface;
        width: auto;

        &:blur {
            padding: 1;
        }
        &:focus-within {
            border: tall $primary;
            padding: 0;
        }

        Button, AbstractInput {
            border: none;
            padding: 0;
            height: 1;

            &:focus {
                color: $accent;
                text-style: none;
            }
        }
        & > TargetButton {
            min-width: 1;
            max-width: 3;
        }

        & > AbstractInput {
            padding: 0 2;
            &.-invalid {
                color: $error;
                text-style: italic;
            }
            &:focus {
                tint: $primary 2%;
            }
        }
    }
    & > BaseOverlay {
        border: round $secondary;
        overlay: screen !important;
        constrain: inside;
        position: absolute;
        height: auto;
        width: auto;
        background: $surface;
        box-sizing: content-box;
        opacity: 0;

        &:focus,
        &:focus-within {
            border: round $primary;
        }

        & > BaseOverlayWidget {
            width: 40;
            height: auto;
        }
    }
}

BINDINGS class-attribute

BINDINGS: list[BindingType] = [
    Binding(
        "ctrl+shift+d",
        "clear",
        "Clear",
        tooltip="Clear end and start datetime.",
    ),
    Binding(
        "ctrl+t",
        "target_default_start",
        "Start To Today",
        tooltip="Set the start datetime to now.",
    ),
    Binding(
        "alt+ctrl+t",
        "target_default_end",
        "End To Today",
        tooltip="Set the end datetime to now or the start datetime.",
    ),
]

All bindings for DateTimeRangePicker.

Key(s) Description
ctrl+shift+d Clear end and start datetime.
ctrl+t Set the start datetime to now.
alt+ctrl+t Set the end datetime to now or the start datetime.

start_dt class-attribute instance-attribute

start_dt = var[LocalDateTime | None](None, init=False)

Picker start datetime. Bound to all the parent widgets.

end_dt class-attribute instance-attribute

end_dt = var[LocalDateTime | None](None, init=False)

Picker end datetime. Bound to all the parent widgets.

start_date class-attribute instance-attribute

start_date = var[Date | None](None, init=False)

Start date dynamically computed depending on start datetime.

end_date class-attribute instance-attribute

end_date = var[Date | None](None, init=False)

End date dynamically computed depending on the end datetime.

DTRangeChanged dataclass

Bases: BaseMessage

Message sent when the datetime range has changed.

Source code in src/textual_timepiece/pickers/_timerange_picker.py
375
376
377
378
379
380
381
@dataclass
class DTRangeChanged(BaseMessage):
    """Message sent when the datetime range has changed."""

    widget: DateTimeRangePicker
    start: LocalDateTime | None
    end: LocalDateTime | None

adjust_start_date

adjust_start_date(date: Date | None) -> None

Set or clear the current start date depending on the input.

Source code in src/textual_timepiece/pickers/_timerange_picker.py
534
535
536
537
538
539
540
541
def adjust_start_date(self, date: Date | None) -> None:
    """Set or clear the current start date depending on the input."""
    if self.start_dt and date:
        self.start_dt = self.start_dt.replace_date(date)
    elif date:
        self.start_dt = date.at(Time())
    else:
        self.start_dt = date

adjust_end_date

adjust_end_date(date: Date | None) -> None

Set or clear the current end date depending on the input.

Source code in src/textual_timepiece/pickers/_timerange_picker.py
543
544
545
546
547
548
549
550
def adjust_end_date(self, date: Date | None) -> None:
    """Set or clear the current end date depending on the input."""
    if self.end_dt and date:
        self.end_dt = self.end_dt.replace_date(date)
    elif date:
        self.end_dt = date.at(Time())
    else:
        self.end_dt = date

action_clear

action_clear() -> None

Clear the start and end datetimes.

Source code in src/textual_timepiece/pickers/_timerange_picker.py
552
553
554
555
def action_clear(self) -> None:
    """Clear the start and end datetimes."""
    self.start_dt = None
    self.end_dt = None

disable_start

disable_start(*, disable: bool = True) -> Self

Utility method to disable start input widgets.

Source code in src/textual_timepiece/pickers/_timerange_picker.py
601
602
603
604
605
606
def disable_start(self, *, disable: bool = True) -> Self:
    """Utility method to disable start input widgets."""
    self.start_input.disabled = disable
    self.overlay.query_one("#start-column", Vertical).disabled = disable
    self.query_one("#target-default-start", Button).disabled = disable
    return self

disable_end

disable_end(*, disable: bool = True) -> Self

Utility method to disable end input widgets.

Source code in src/textual_timepiece/pickers/_timerange_picker.py
608
609
610
611
612
613
def disable_end(self, *, disable: bool = True) -> Self:
    """Utility method to disable end input widgets."""
    self.end_input.disabled = disable
    self.overlay.query_one("#end-column", Vertical).disabled = disable
    self.query_one("#target-default-end", Button).disabled = disable
    return self

DateTimeDurationPicker

DateTimeDurationPickerApp ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ 2025-02-1012:13:0029:00:00🔓2025-02-1117:13:00 ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ───────────────────────────────────────────────────────────────────────────── HoursMinutesSecondsHoursMinutesSeconds +1+4+15+30+15+30+1+4+15+30+15+30 -1-4-15-30-15-30-1-4-15-30-15-30 February 2025February 2025 MonTueWedThuFriSatSunMonTueWedThuFriSatSun   1  2  1  2   3  4  5  6  7  8  9  3  4  5  6  7  8  9  10 11 12 13 14 15 16 10 11 12 13 14 15 16  17 18 19 20 21 22 23 17 18 19 20 21 22 23  24 25 26 27 28 24 25 26 27 28 ─────────────────────────────────────────────────────────────────────────────

textual_timepiece.pickers.DateTimeDurationPicker

Bases: DateTimeRangePicker

Datetime range with a duration input in the middle.

Duration display up to 99:99:99. Use the DateTimeRangePicker picker if a longer duration is required.

PARAMETER DESCRIPTION
start

Initial start datetime for the picker.

TYPE: LocalDateTime | None DEFAULT: None

end

Initial end datetime for the picker.

TYPE: LocalDateTime | None DEFAULT: None

name

Name for the widget.

TYPE: str | None DEFAULT: None

id

DOM identifier for the widget.

TYPE: str | None DEFAULT: None

classes

CSS classes for the widget

TYPE: str | None DEFAULT: None

time_range

Time range to restrict the datetimes to. If provided the picker lock will be permanently on for the widgets lifetime or re-enabled programmatically.

TYPE: TimeDelta | None DEFAULT: None

disabled

Whether to disable the widget

TYPE: bool DEFAULT: False

tooltip

Tooltip to show on hover.

TYPE: str | None DEFAULT: None

ATTRIBUTE DESCRIPTION
DEFAULT_CSS

TYPE: str

BINDINGS

All bindings for DateTimeRangePicker.

TYPE: list[BindingType]

duration

Duration between start and end datetimes. Computed dynamically.

Source code in src/textual_timepiece/pickers/_timerange_picker.py
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
class DateTimeDurationPicker(DateTimeRangePicker):
    """Datetime range with a duration input in the middle.

    Duration display up to 99:99:99. Use the DateTimeRangePicker picker if a
    longer duration is required.

    Params:
        start: Initial start datetime for the picker.
        end: Initial end datetime for the picker.
        name: Name for the widget.
        id: DOM identifier for the widget.
        classes: CSS classes for the widget
        time_range: Time range to restrict the datetimes to. If provided the
            picker lock will be permanently on for the widgets lifetime or
            re-enabled programmatically.
        disabled: Whether to disable the widget
        tooltip: Tooltip to show on hover.
    """

    duration = var[TimeDelta | None](None, init=False)
    """Duration between start and end datetimes. Computed dynamically."""

    async def _on_mount(self) -> None:  # type: ignore[override] # NOTE: Need to mount extra widget
        """Overrides the compose method in order to a duration input."""
        await self.query_exactly_one("#input-control", Horizontal).mount(
            DurationInput(self.duration, id="duration-input").data_bind(
                DateTimeDurationPicker.duration
            ),
            after=1,
        )

    def _compute_duration(self) -> TimeDelta:
        if self.start_dt is None or self.end_dt is None:
            return TimeDelta()
        return self.end_dt.difference(self.start_dt, ignore_dst=True)

    @on(DurationInput.DurationChanged)
    def _new_duration(self, message: DurationInput.DurationChanged) -> None:
        message.stop()
        with message.control.prevent(DurationInput.DurationChanged):
            if message.duration is None:
                self.end_dt = None
            elif self.start_dt:
                self.end_dt = self.start_dt.add(
                    seconds=message.duration.in_seconds(),
                    ignore_dst=True,
                )
            elif self.end_dt:
                self.start_dt = self.end_dt.subtract(
                    seconds=message.duration.in_seconds(),
                    ignore_dst=True,
                )

    @on(Button.Pressed, "#lock-button")
    def _lock_duration(self, message: Button.Pressed) -> None:
        message.stop()
        if self.start_date:
            self.duration_input.disabled = cast(
                LockButton, message.button
            ).locked

    @cached_property
    def duration_input(self) -> DurationInput:
        return cast(DurationInput, self.query_exactly_one(DurationInput))

DEFAULT_CSS class-attribute

AbstractPicker {
    layers: base dialog;
    layout: vertical;
    height: 3;
    width: auto;

    &.mini {
        max-height: 1;
        & > #input-control {
            border: none;
            height: 1;
            padding: 0;

            &:blur {
                padding: 0;
            }
            &:focus-within {
                padding: 0;
                border: none;
            }
            Button, AbstractInput {
                border: none;
                padding: 0;
                height: 1;

                &:focus {
                    color: $accent;
                    text-style: none;
                }
                &:disabled {
                    opacity: 50%;
                    text-style: italic;
                }
            }
        }
    }

    & > #input-control {
        background: $surface;
        width: auto;

        &:blur {
            padding: 1;
        }
        &:focus-within {
            border: tall $primary;
            padding: 0;
        }

        Button, AbstractInput {
            border: none;
            padding: 0;
            height: 1;

            &:focus {
                color: $accent;
                text-style: none;
            }
        }
        & > TargetButton {
            min-width: 1;
            max-width: 3;
        }

        & > AbstractInput {
            padding: 0 2;
            &.-invalid {
                color: $error;
                text-style: italic;
            }
            &:focus {
                tint: $primary 2%;
            }
        }
    }
    & > BaseOverlay {
        border: round $secondary;
        overlay: screen !important;
        constrain: inside;
        position: absolute;
        height: auto;
        width: auto;
        background: $surface;
        box-sizing: content-box;
        opacity: 0;

        &:focus,
        &:focus-within {
            border: round $primary;
        }

        & > BaseOverlayWidget {
            width: 40;
            height: auto;
        }
    }
}

BINDINGS class-attribute

BINDINGS: list[BindingType] = [
    Binding(
        "ctrl+shift+d",
        "clear",
        "Clear",
        tooltip="Clear end and start datetime.",
    ),
    Binding(
        "ctrl+t",
        "target_default_start",
        "Start To Today",
        tooltip="Set the start datetime to now.",
    ),
    Binding(
        "alt+ctrl+t",
        "target_default_end",
        "End To Today",
        tooltip="Set the end datetime to now or the start datetime.",
    ),
]

All bindings for DateTimeRangePicker.

Key(s) Description
ctrl+shift+d Clear end and start datetime.
ctrl+t Set the start datetime to now.
alt+ctrl+t Set the end datetime to now or the start datetime.

duration class-attribute instance-attribute

duration = var[TimeDelta | None](None, init=False)

Duration between start and end datetimes. Computed dynamically.