229 lines
6.2 KiB
Python
229 lines
6.2 KiB
Python
"""Provides a Textual application header widget."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
|
|
from rich.text import Text
|
|
|
|
from textual.app import ComposeResult, RenderResult
|
|
from textual.content import Content
|
|
from textual.dom import NoScreen
|
|
from textual.events import Click, Mount
|
|
from textual.reactive import Reactive
|
|
from textual.widget import Widget
|
|
from textual.widgets import Static
|
|
|
|
|
|
class HeaderIcon(Widget):
|
|
"""Display an 'icon' on the left of the header."""
|
|
|
|
DEFAULT_CSS = """
|
|
HeaderIcon {
|
|
dock: left;
|
|
padding: 0 1;
|
|
width: 8;
|
|
content-align: left middle;
|
|
}
|
|
|
|
HeaderIcon:hover {
|
|
background: $foreground 10%;
|
|
}
|
|
"""
|
|
|
|
icon = Reactive("⭘")
|
|
"""The character to use as the icon within the header."""
|
|
|
|
def on_mount(self) -> None:
|
|
if self.app.ENABLE_COMMAND_PALETTE:
|
|
self.tooltip = "Open the command palette"
|
|
else:
|
|
self.disabled = True
|
|
|
|
async def on_click(self, event: Click) -> None:
|
|
"""Launch the command palette when icon is clicked."""
|
|
event.stop()
|
|
await self.run_action("app.command_palette")
|
|
|
|
def render(self) -> RenderResult:
|
|
"""Render the header icon.
|
|
|
|
Returns:
|
|
The rendered icon.
|
|
"""
|
|
return self.icon
|
|
|
|
|
|
class HeaderClockSpace(Widget):
|
|
"""The space taken up by the clock on the right of the header."""
|
|
|
|
DEFAULT_CSS = """
|
|
HeaderClockSpace {
|
|
dock: right;
|
|
width: 10;
|
|
padding: 0 1;
|
|
}
|
|
"""
|
|
|
|
def render(self) -> RenderResult:
|
|
"""Render the header clock space.
|
|
|
|
Returns:
|
|
The rendered space.
|
|
"""
|
|
return ""
|
|
|
|
|
|
class HeaderClock(HeaderClockSpace):
|
|
"""Display a clock on the right of the header."""
|
|
|
|
DEFAULT_CSS = """
|
|
HeaderClock {
|
|
background: $foreground-darken-1 5%;
|
|
color: $foreground;
|
|
text-opacity: 85%;
|
|
content-align: center middle;
|
|
}
|
|
"""
|
|
|
|
time_format: Reactive[str] = Reactive("%X")
|
|
|
|
def _on_mount(self, _: Mount) -> None:
|
|
self.set_interval(1, callback=self.refresh, name="update header clock")
|
|
|
|
def render(self) -> RenderResult:
|
|
"""Render the header clock.
|
|
|
|
Returns:
|
|
The rendered clock.
|
|
"""
|
|
return Text(datetime.now().time().strftime(self.time_format))
|
|
|
|
|
|
class HeaderTitle(Static):
|
|
"""Display the title / subtitle in the header."""
|
|
|
|
DEFAULT_CSS = """
|
|
HeaderTitle {
|
|
text-wrap: nowrap;
|
|
text-overflow: ellipsis;
|
|
content-align: center middle;
|
|
width: 100%;
|
|
}
|
|
"""
|
|
|
|
|
|
class Header(Widget):
|
|
"""A header widget with icon and clock."""
|
|
|
|
DEFAULT_CSS = """
|
|
Header {
|
|
dock: top;
|
|
width: 100%;
|
|
background: $panel;
|
|
color: $foreground;
|
|
height: 1;
|
|
}
|
|
Header.-tall {
|
|
height: 3;
|
|
}
|
|
"""
|
|
|
|
DEFAULT_CLASSES = ""
|
|
|
|
tall: Reactive[bool] = Reactive(False)
|
|
"""Set to `True` for a taller header or `False` for a single line header."""
|
|
|
|
icon: Reactive[str] = Reactive("⭘")
|
|
"""A character for the icon at the top left."""
|
|
|
|
time_format: Reactive[str] = Reactive("%X")
|
|
"""Time format of the clock."""
|
|
|
|
def __init__(
|
|
self,
|
|
show_clock: bool = False,
|
|
*,
|
|
name: str | None = None,
|
|
id: str | None = None,
|
|
classes: str | None = None,
|
|
icon: str | None = None,
|
|
time_format: str | None = None,
|
|
):
|
|
"""Initialise the header widget.
|
|
|
|
Args:
|
|
show_clock: ``True`` if the clock should be shown on the right of the header.
|
|
name: The name of the header widget.
|
|
id: The ID of the header widget in the DOM.
|
|
classes: The CSS classes of the header widget.
|
|
icon: Single character to use as an icon, or `None` for default.
|
|
time_format: Time format (used by strftime) for clock, or `None` for default.
|
|
"""
|
|
super().__init__(name=name, id=id, classes=classes)
|
|
self._show_clock = show_clock
|
|
if icon is not None:
|
|
self.icon = icon
|
|
if time_format is not None:
|
|
self.time_format = time_format
|
|
|
|
def compose(self) -> ComposeResult:
|
|
yield HeaderIcon().data_bind(Header.icon)
|
|
yield HeaderTitle()
|
|
yield (
|
|
HeaderClock().data_bind(Header.time_format)
|
|
if self._show_clock
|
|
else HeaderClockSpace()
|
|
)
|
|
|
|
def watch_tall(self, tall: bool) -> None:
|
|
self.set_class(tall, "-tall")
|
|
|
|
def _on_click(self):
|
|
self.toggle_class("-tall")
|
|
|
|
def format_title(self) -> Content:
|
|
"""Format the title and subtitle.
|
|
|
|
Defers to [App.format_title][textual.app.App.format_title] by default.
|
|
Override this method if you want to customize how the title is displayed in the header.
|
|
|
|
Returns:
|
|
Content for title display.
|
|
"""
|
|
return self.app.format_title(self.screen_title, self.screen_sub_title)
|
|
|
|
@property
|
|
def screen_title(self) -> str:
|
|
"""The title that this header will display.
|
|
|
|
This depends on [`Screen.title`][textual.screen.Screen.title] and [`App.title`][textual.app.App.title].
|
|
"""
|
|
screen_title = self.screen.title
|
|
title = screen_title if screen_title is not None else self.app.title
|
|
return title
|
|
|
|
@property
|
|
def screen_sub_title(self) -> str:
|
|
"""The sub-title that this header will display.
|
|
|
|
This depends on [`Screen.sub_title`][textual.screen.Screen.sub_title] and [`App.sub_title`][textual.app.App.sub_title].
|
|
"""
|
|
screen_sub_title = self.screen.sub_title
|
|
sub_title = (
|
|
screen_sub_title if screen_sub_title is not None else self.app.sub_title
|
|
)
|
|
return sub_title
|
|
|
|
def _on_mount(self, _: Mount) -> None:
|
|
async def set_title() -> None:
|
|
try:
|
|
self.query_one(HeaderTitle).update(self.format_title())
|
|
except NoScreen:
|
|
pass
|
|
|
|
self.watch(self.app, "title", set_title)
|
|
self.watch(self.app, "sub_title", set_title)
|
|
self.watch(self.screen, "title", set_title)
|
|
self.watch(self.screen, "sub_title", set_title)
|