From ca16b2d659d846d691b9297d8d8f510df6662bfd Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 19 Oct 2022 15:03:05 +0100 Subject: [PATCH 01/17] adds demo --- src/textual/__main__.py | 6 ++ src/textual/_animator.py | 1 + src/textual/app.py | 14 ++--- src/textual/color.py | 16 ++--- src/textual/css/_style_properties.py | 1 + src/textual/css/scalar_animation.py | 11 ++-- src/textual/css/styles.py | 3 - src/textual/demo.css | 89 +++++++++++++++++++++++++++ src/textual/demo.py | 90 ++++++++++++++++++++++++++++ src/textual/design.py | 8 +-- src/textual/widgets/_header.py | 4 +- src/textual/widgets/_text_log.py | 9 ++- 12 files changed, 224 insertions(+), 28 deletions(-) create mode 100644 src/textual/__main__.py create mode 100644 src/textual/demo.css create mode 100644 src/textual/demo.py diff --git a/src/textual/__main__.py b/src/textual/__main__.py new file mode 100644 index 000000000..46d0f0c7e --- /dev/null +++ b/src/textual/__main__.py @@ -0,0 +1,6 @@ +from .demo import DemoApp + + +app = DemoApp() +if __name__ == "__main__": + app.run() diff --git a/src/textual/_animator.py b/src/textual/_animator.py index b628a23f7..0b8ebb1ab 100644 --- a/src/textual/_animator.py +++ b/src/textual/_animator.py @@ -86,6 +86,7 @@ class SimpleAnimation(Animation): assert isinstance( self.end_value, (int, float) ), f"`end_value` must be float, not {self.end_value!r}" + if self.end_value > self.start_value: eased_factor = self.easing(factor) value = ( diff --git a/src/textual/app.py b/src/textual/app.py index f66050554..5e813542d 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -139,18 +139,18 @@ class App(Generic[ReturnType], DOMNode): """ SCREENS: dict[str, Screen] = {} - _BASE_PATH: str | None = None CSS_PATH: CSSPathType = None + TITLE: str | None = None + SUB_TITLE: str | None = None - title: Reactive[str] = Reactive("Textual") + title: Reactive[str] = Reactive("") sub_title: Reactive[str] = Reactive("") dark: Reactive[bool] = Reactive(True) def __init__( self, driver_class: Type[Driver] | None = None, - title: str | None = None, css_path: CSSPathType = None, watch_css: bool = False, ): @@ -189,10 +189,10 @@ class App(Generic[ReturnType], DOMNode): self._animator = Animator(self) self._animate = self._animator.bind(self) self.mouse_position = Offset(0, 0) - if title is None: - self.title = f"{self.__class__.__name__}" - else: - self.title = title + self.title = ( + self.TITLE if self.TITLE is not None else f"{self.__class__.__name__}" + ) + self.sub_title = self.SUB_TITLE if self.SUB_TITLE is not None else "" self._logger = Logger(self._log) diff --git a/src/textual/color.py b/src/textual/color.py index 16108e71d..e3849cff7 100644 --- a/src/textual/color.py +++ b/src/textual/color.py @@ -341,7 +341,9 @@ class Color(NamedTuple): r, g, b, _ = self return Color(r, g, b, alpha) - def blend(self, destination: Color, factor: float, alpha: float = 1) -> Color: + def blend( + self, destination: Color, factor: float, alpha: float | None = None + ) -> Color: """Generate a new color between two colors. Args: @@ -353,21 +355,21 @@ class Color(NamedTuple): Color: A new color. """ if factor == 0: - return self + return self if alpha is None else self.with_alpha(alpha) elif factor == 1: - return destination - r1, g1, b1, _ = self - r2, g2, b2, _ = destination + return destination if alpha is None else destination.with_alpha(alpha) + r1, g1, b1, a1 = self + r2, g2, b2, a2 = destination return Color( int(r1 + (r2 - r1) * factor), int(g1 + (g2 - g1) * factor), int(b1 + (b2 - b1) * factor), - alpha, + a1 + (a2 - a1) * factor if alpha is None else alpha, ) def __add__(self, other: object) -> Color: if isinstance(other, Color): - new_color = self.blend(other, other.a) + new_color = self.blend(other, other.a, alpha=1.0) return new_color return NotImplemented diff --git a/src/textual/css/_style_properties.py b/src/textual/css/_style_properties.py index 6a2bffe52..5770add70 100644 --- a/src/textual/css/_style_properties.py +++ b/src/textual/css/_style_properties.py @@ -856,6 +856,7 @@ class ColorProperty: elif isinstance(color, Color): if obj.set_rule(self.name, color): obj.refresh(children=self._is_background) + elif isinstance(color, str): alpha = 1.0 parsed_color = Color(255, 255, 255) diff --git a/src/textual/css/scalar_animation.py b/src/textual/css/scalar_animation.py index c584ef692..697f94405 100644 --- a/src/textual/css/scalar_animation.py +++ b/src/textual/css/scalar_animation.py @@ -58,10 +58,13 @@ class ScalarAnimation(Animation): setattr(self.styles, self.attribute, self.final_value) return True - offset = self.start + (self.destination - self.start) * eased_factor - current = self.styles._rules[self.attribute] - if current != offset: - setattr(self.styles, f"{self.attribute}", offset) + if hasattr(self.start, "blend"): + value = self.start.blend(self.destination, eased_factor) + else: + value = self.start + (self.destination - self.start) * eased_factor + current = self.styles._rules.get(self.attribute) + if current != value: + setattr(self.styles, f"{self.attribute}", value) return False diff --git a/src/textual/css/styles.py b/src/textual/css/styles.py index dd4b8d4b6..cbb99b509 100644 --- a/src/textual/css/styles.py +++ b/src/textual/css/styles.py @@ -644,9 +644,6 @@ class Styles(StylesBase): easing: EasingFunction, on_complete: CallbackType | None = None, ) -> ScalarAnimation | None: - # from ..widget import Widget - # node = self.node - # assert isinstance(self.node, Widget) if isinstance(value, ScalarOffset): return ScalarAnimation( self.node, diff --git a/src/textual/demo.css b/src/textual/demo.css new file mode 100644 index 000000000..93f32ab1d --- /dev/null +++ b/src/textual/demo.css @@ -0,0 +1,89 @@ + * { + transition:background 300ms linear, color 300ms linear; +} + +Screen { + layers: base overlay notes; + overflow: hidden; + +} + + +Sidebar { + width: 30; + background: $panel; + transition: offset 500ms in_out_cubic; + layer: overlay; +} + +Sidebar.-hidden { + offset-x: -100%; +} + +Sidebar Title { + + background: $boost; + color: $text; + padding: 2 4; + border-right: vkey $background; + dock: top; + text-align: center; +} + + +Body { + height: 100%; + overflow-y: auto; + align: center middle; + background: $surface; +} + +Welcome { + background: $panel; + height: auto; + margin: 1 2; + padding: 1 2; + max-width: 60; + border: wide $accent; +} + +#dark-switcher { + width: 100%; + height: 5; + align: center middle; +} + +DarkSwitch { + + background: $panel-lighten-1; + dock: bottom; + height: auto; + border-right: vkey $background; +} + +DarkSwitch .label { + padding: 1 2; + color: $text-muted; +} + + +Screen > Container { + height: 100%; + overflow: hidden; +} + +TextLog { + background: $surface; + color: rgba(255,255,255,0.9); + height: 50vh; + dock: bottom; + layer: notes; + + offset-y: 0; + transition: offset 200ms in_out_cubic; +} + +TextLog.-hidden { + offset-y: 100%; + transition: offset 200ms in_out_cubic; +} diff --git a/src/textual/demo.py b/src/textual/demo.py new file mode 100644 index 000000000..408036860 --- /dev/null +++ b/src/textual/demo.py @@ -0,0 +1,90 @@ +from rich.console import RenderableType +from rich.markdown import Markdown +from rich.text import Text + +from textual.app import App, ComposeResult + +from textual.containers import Container, Horizontal +from textual.reactive import reactive, watch +from textual.widgets import Header, Footer, Static, Button, Checkbox, TextLog + + +WELCOME_MD = """ + +## Textual Demo + +Welcome to the Textual demo! + +- + +""" + + +class Body(Container): + pass + + +class Title(Static): + pass + + +class DarkSwitch(Horizontal): + def compose(self) -> ComposeResult: + yield Checkbox(value=self.app.dark) + yield Static("Dark mode", classes="label") + + def on_mount(self) -> None: + watch(self.app, "dark", self.on_dark_change) + + def on_dark_change(self, dark: bool) -> None: + self.query_one(Checkbox).value = dark + + def on_checkbox_changed(self, event: Checkbox.Changed) -> None: + self.app.dark = event.value + + +class Welcome(Container): + def compose(self) -> ComposeResult: + yield Static(Markdown(WELCOME_MD)) + + +class Sidebar(Container): + def compose(self) -> ComposeResult: + yield Title("Textual Demo") + yield Container() + yield DarkSwitch() + + +class DemoApp(App): + CSS_PATH = "demo.css" + TITLE = "Textual Demo" + BINDINGS = [ + ("s", "app.toggle_class('Sidebar', '-hidden')", "Sidebar"), + ("d", "app.toggle_dark", "Toggle Dark mode"), + ("n", "app.toggle_class('TextLog', '-hidden')", "Notes"), + ] + + show_sidebar = reactive(False) + + def add_note(self, renderable: RenderableType) -> None: + self.query_one(TextLog).write(renderable) + + def on_mount(self) -> None: + self.add_note("[b]Textual Nodes") + + def compose(self) -> ComposeResult: + yield Container( + Sidebar(), + Header(), + TextLog(classes="-hidden", wrap=False, highlight=True, markup=True), + Body(Welcome()), + ) + yield Footer() + + def on_dark_switch_toggle(self) -> None: + self.dark = not self.dark + + +app = DemoApp() +if __name__ == "__main__": + app.run() diff --git a/src/textual/design.py b/src/textual/design.py index 268bdb621..35650e9bf 100644 --- a/src/textual/design.py +++ b/src/textual/design.py @@ -128,7 +128,7 @@ class ColorSystem: boost = self.boost or background.get_contrast_text(1.0).with_alpha(0.04) if self.panel is None: - panel = surface.blend(primary, 0.1) + panel = surface.blend(primary, 0.1, alpha=1) if dark: panel += boost else: @@ -154,7 +154,7 @@ class ColorSystem: yield (f"{label}{'-' + str(abs(n)) if n else ''}"), n * luminosity_step # Color names and color - COLORS = [ + COLORS: list[tuple[str, Color]] = [ ("primary", primary), ("secondary", secondary), ("primary-background", primary), @@ -178,9 +178,9 @@ class ColorSystem: spread = luminosity_spread for shade_name, luminosity_delta in luminosity_range(spread): if is_dark_shade: - dark_background = background.blend(color, 0.15) + dark_background = background.blend(color, 0.15, alpha=1.0) shade_color = dark_background.blend( - WHITE, spread + luminosity_delta + WHITE, spread + luminosity_delta, alpha=1.0 ).clamped colors[f"{name}{shade_name}"] = shade_color.hex else: diff --git a/src/textual/widgets/_header.py b/src/textual/widgets/_header.py index b75418a1a..d22c04c89 100644 --- a/src/textual/widgets/_header.py +++ b/src/textual/widgets/_header.py @@ -58,8 +58,8 @@ class HeaderTitle(Widget): } """ - text: Reactive[str] = Reactive("Hello World") - sub_text = Reactive("Test") + text: Reactive[str] = Reactive("") + sub_text = Reactive("") def render(self) -> Text: text = Text(self.text, no_wrap=True, overflow="ellipsis") diff --git a/src/textual/widgets/_text_log.py b/src/textual/widgets/_text_log.py index 406914fb4..db39ae985 100644 --- a/src/textual/widgets/_text_log.py +++ b/src/textual/widgets/_text_log.py @@ -30,6 +30,7 @@ class TextLog(ScrollView, can_focus=True): min_width: var[int] = var(78) wrap: var[bool] = var(False) highlight: var[bool] = var(False) + markup: var[bool] = var(False) def __init__( self, @@ -38,6 +39,7 @@ class TextLog(ScrollView, can_focus=True): min_width: int = 78, wrap: bool = False, highlight: bool = False, + markup: bool = False, name: str | None = None, id: str | None = None, classes: str | None = None, @@ -51,6 +53,7 @@ class TextLog(ScrollView, can_focus=True): self.min_width = min_width self.wrap = wrap self.highlight = highlight + self.markup = markup self.highlighter = ReprHighlighter() def _on_styles_updated(self) -> None: @@ -68,6 +71,8 @@ class TextLog(ScrollView, can_focus=True): renderable = Pretty(content) else: if isinstance(content, str): + if self.markup: + content = Text.from_markup(content) if self.highlight: renderable = self.highlighter(content) else: @@ -102,7 +107,9 @@ class TextLog(ScrollView, can_focus=True): def render_line(self, y: int) -> list[Segment]: scroll_x, scroll_y = self.scroll_offset - return self._render_line(scroll_y + y, scroll_x, self.size.width) + line = self._render_line(scroll_y + y, scroll_x, self.size.width) + line = list(Segment.apply_style(line, post_style=self.rich_style)) + return line def render_lines(self, crop: Region) -> Lines: """Render the widget in to lines. From 585fc8b6c7bc67a04c621d411f07084d8d8fd4e9 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 19 Oct 2022 18:00:36 +0100 Subject: [PATCH 02/17] more demo --- src/textual/app.py | 1 + src/textual/box_model.py | 2 + src/textual/color.py | 4 - src/textual/css/parse.py | 4 + src/textual/demo.css | 114 ++++++++++++++++++++------ src/textual/demo.py | 132 ++++++++++++++++++++++++++++--- src/textual/widgets/_text_log.py | 5 +- 7 files changed, 222 insertions(+), 40 deletions(-) diff --git a/src/textual/app.py b/src/textual/app.py index 5e813542d..9b5b13753 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -1339,6 +1339,7 @@ class App(Generic[ReturnType], DOMNode): Returns: bool: True if the event has handled. """ + print("ACTION", action, default_namespace) if isinstance(action, str): target, params = actions.parse(action) else: diff --git a/src/textual/box_model.py b/src/textual/box_model.py index adfc49c36..7271bef54 100644 --- a/src/textual/box_model.py +++ b/src/textual/box_model.py @@ -80,6 +80,8 @@ def get_box_model( max_width = styles.max_width.resolve_dimension( content_container, viewport, fraction_unit ) + if is_border_box: + max_width -= gutter.width content_width = min(content_width, max_width) content_width = max(Fraction(0), content_width) diff --git a/src/textual/color.py b/src/textual/color.py index e3849cff7..71fcdc11a 100644 --- a/src/textual/color.py +++ b/src/textual/color.py @@ -354,10 +354,6 @@ class Color(NamedTuple): Returns: Color: A new color. """ - if factor == 0: - return self if alpha is None else self.with_alpha(alpha) - elif factor == 1: - return destination if alpha is None else destination.with_alpha(alpha) r1, g1, b1, a1 = self r2, g2, b2, a2 = destination return Color( diff --git a/src/textual/css/parse.py b/src/textual/css/parse.py index b26f171db..fdfca677a 100644 --- a/src/textual/css/parse.py +++ b/src/textual/css/parse.py @@ -36,6 +36,10 @@ SELECTOR_MAP: dict[str, tuple[SelectorType, Specificity3]] = { @lru_cache(maxsize=1024) def parse_selectors(css_selectors: str) -> tuple[SelectorSet, ...]: + + if not css_selectors.strip(): + return () + tokens = iter(tokenize(css_selectors, "")) get_selector = SELECTOR_MAP.get diff --git a/src/textual/demo.css b/src/textual/demo.css index 93f32ab1d..86b60e373 100644 --- a/src/textual/demo.css +++ b/src/textual/demo.css @@ -1,19 +1,16 @@ - * { - transition:background 300ms linear, color 300ms linear; -} Screen { layers: base overlay notes; - overflow: hidden; - + overflow: hidden; } Sidebar { - width: 30; + width: 40; background: $panel; transition: offset 500ms in_out_cubic; layer: overlay; + } Sidebar.-hidden { @@ -21,7 +18,6 @@ Sidebar.-hidden { } Sidebar Title { - background: $boost; color: $text; padding: 2 4; @@ -31,37 +27,71 @@ Sidebar Title { } +OptionGroup { + background: $boost; + height: 1fr; + border-right: vkey $background; +} + +Option { + margin: 1 0 0 1; + height: 3; + padding: 1 2; + background: $boost; + border: tall $panel; + text-align: center; +} + +Option:hover { + background: $primary 20%; + color: $text; +} + Body { height: 100%; overflow-y: auto; + width: 100%; + background: $surface; + +} + +AboveFold { + width: 100%; + height: 100%; align: center middle; - background: $surface; } Welcome { - background: $panel; - height: auto; - margin: 1 2; - padding: 1 2; - max-width: 60; - border: wide $accent; + background: $boost; + height: auto; + max-width: 100; + min-width: 40; + border: wide $primary; + padding: 1 2; + box-sizing: border-box; } -#dark-switcher { +Welcome Button { width: 100%; - height: 5; - align: center middle; + margin-top: 1; } -DarkSwitch { - - background: $panel-lighten-1; +Column { + height: auto; + align: center top; +} + + +DarkSwitch { + background: $panel; + padding: 1; dock: bottom; height: auto; border-right: vkey $background; } DarkSwitch .label { + padding: 1 2; color: $text-muted; } @@ -74,16 +104,54 @@ Screen > Container { TextLog { background: $surface; - color: rgba(255,255,255,0.9); + color: $text; height: 50vh; dock: bottom; layer: notes; - + border-top: hkey $primary; offset-y: 0; - transition: offset 200ms in_out_cubic; + transition: offset 200ms in_out_cubic; + padding: 0 1; } TextLog.-hidden { offset-y: 100%; transition: offset 200ms in_out_cubic; } + +Section { + height: auto; + min-width: 40; + margin: 2 2; +} + +SectionTitle { + padding: 1 2; + background: $boost; + text-align: center; + text-style: bold; +} + +Text { + margin: 1 0; +} + +QuickAccess { + width: 30; + dock: left; + +} + +LocationLink { + margin: 1 0 0 1; + height: 1; + padding: 0 2; + background: $boost; + border: tall $surface; + text-align: center; +} + +LocationLink:hover { + background: $primary 20%; + color: $text; +} diff --git a/src/textual/demo.py b/src/textual/demo.py index 408036860..ec22224af 100644 --- a/src/textual/demo.py +++ b/src/textual/demo.py @@ -1,5 +1,6 @@ from rich.console import RenderableType from rich.markdown import Markdown +from rich.syntax import Syntax from rich.text import Text from textual.app import App, ComposeResult @@ -13,19 +14,54 @@ WELCOME_MD = """ ## Textual Demo -Welcome to the Textual demo! +Textual is a framework for creating sophisticated applications with the terminal. -- +Powered by **Rich** + +GITHUB: https://github.com/Textualize/textual """ +CSS_MD = """ + +Textual uses Cascading Stylesheets (CSS) to create Rich interactive User Interfaces. + +- **Easy to learn** - much simpler than browser CSS +- **Live editing** - see your changes without restarting the app! + +Here's an example of some CSS used in this app: + +""" + +EXAMPLE_CSS = """\ +Screen { + layers: base overlay notes; + overflow: hidden; +} + +Sidebar { + width: 40; + background: $panel; + transition: offset 500ms in_out_cubic; + layer: overlay; + +} + +Sidebar.-hidden { + offset-x: -100%; +}""" + class Body(Container): pass class Title(Static): - pass + def action_open_docs(self) -> None: + self.app.bell() + import webbrowser + + webbrowser.open("https://textual.textualize.io") class DarkSwitch(Horizontal): @@ -46,15 +82,73 @@ class DarkSwitch(Horizontal): class Welcome(Container): def compose(self) -> ComposeResult: yield Static(Markdown(WELCOME_MD)) + yield Button("Start", variant="success") + + def on_button_pressed(self, event: Button.Pressed) -> None: + self.app.add_note("[b magenta]Start!") + self.app.query_one(".location-first").scroll_visible(speed=50) + + +class OptionGroup(Container): + pass + + +class SectionTitle(Static): + pass + + +class Option(Static): + def __init__(self, label: str, reveal: str) -> None: + super().__init__(label) + self.reveal = reveal + + def on_click(self) -> None: + self.app.query_one(self.reveal).scroll_visible() + self.app.add_note(f"Scrolling to [b]{self.reveal}[/b]") class Sidebar(Container): def compose(self) -> ComposeResult: - yield Title("Textual Demo") - yield Container() + yield Title("[@click=open_docs]Textual Demo[/]") + yield OptionGroup( + Option("TOP", ".location-top"), + Option("First", ".location-first"), + Option("Baz", ""), + ) + yield DarkSwitch() +class AboveFold(Container): + pass + + +class Section(Container): + pass + + +class Column(Container): + pass + + +class Text(Static): + pass + + +class QuickAccess(Container): + pass + + +class LocationLink(Static): + def __init__(self, label: str, reveal: str) -> None: + super().__init__(label) + self.reveal = reveal + + def on_click(self) -> None: + self.app.query_one(self.reveal).scroll_visible() + self.app.add_note(f"Scrolling to [b]{self.reveal}[/b]") + + class DemoApp(App): CSS_PATH = "demo.css" TITLE = "Textual Demo" @@ -70,20 +164,36 @@ class DemoApp(App): self.query_one(TextLog).write(renderable) def on_mount(self) -> None: - self.add_note("[b]Textual Nodes") + self.add_note("Textual Demo app is running") def compose(self) -> ComposeResult: yield Container( - Sidebar(), + Sidebar(classes="-hidden"), Header(), TextLog(classes="-hidden", wrap=False, highlight=True, markup=True), - Body(Welcome()), + Body( + QuickAccess( + LocationLink("TOP", ".location-top"), + LocationLink("First", ".location-first"), + LocationLink("Baz", ""), + ), + AboveFold(Welcome(), classes="location-top"), + Column( + Section( + SectionTitle("CSS"), + Text(Markdown(CSS_MD)), + Static( + Syntax( + EXAMPLE_CSS, "css", theme="material", line_numbers=True + ) + ), + ), + classes="location-first", + ), + ), ) yield Footer() - def on_dark_switch_toggle(self) -> None: - self.dark = not self.dark - app = DemoApp() if __name__ == "__main__": diff --git a/src/textual/widgets/_text_log.py b/src/textual/widgets/_text_log.py index db39ae985..05f1f0d5e 100644 --- a/src/textual/widgets/_text_log.py +++ b/src/textual/widgets/_text_log.py @@ -86,8 +86,9 @@ class TextLog(ScrollView, can_focus=True): render_options = console.options.update_width(width) if not self.wrap: render_options = render_options.update(overflow="ignore", no_wrap=True) - segments = self.app.console.render(renderable, render_options) + segments = self.app.console.render(renderable, render_options.update_width(80)) lines = list(Segment.split_lines(segments)) + self.max_width = max( self.max_width, max(sum(segment.cell_length for segment in _line) for _line in lines), @@ -108,7 +109,7 @@ class TextLog(ScrollView, can_focus=True): def render_line(self, y: int) -> list[Segment]: scroll_x, scroll_y = self.scroll_offset line = self._render_line(scroll_y + y, scroll_x, self.size.width) - line = list(Segment.apply_style(line, post_style=self.rich_style)) + line = list(Segment.apply_style(line, self.rich_style)) return line def render_lines(self, crop: Region) -> Lines: From b56a87b9415c6fb31f68d61106097b33c5c04b0e Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 19 Oct 2022 21:07:26 +0100 Subject: [PATCH 03/17] more demo --- src/textual/demo.css | 53 ++++++++-- src/textual/demo.py | 170 ++++++++++++++++++++++++++++----- src/textual/geometry.py | 22 +++-- src/textual/widget.py | 10 +- src/textual/widgets/_header.py | 4 +- 5 files changed, 213 insertions(+), 46 deletions(-) diff --git a/src/textual/demo.css b/src/textual/demo.css index 86b60e373..14680492d 100644 --- a/src/textual/demo.css +++ b/src/textual/demo.css @@ -49,7 +49,7 @@ Option:hover { Body { height: 100%; - overflow-y: auto; + overflow-y: scroll; width: 100%; background: $surface; @@ -68,6 +68,7 @@ Welcome { min-width: 40; border: wide $primary; padding: 1 2; + margin: 1 2; box-sizing: border-box; } @@ -110,19 +111,20 @@ TextLog { layer: notes; border-top: hkey $primary; offset-y: 0; - transition: offset 200ms in_out_cubic; + transition: offset 400ms in_out_cubic; padding: 0 1; } TextLog.-hidden { offset-y: 100%; - transition: offset 200ms in_out_cubic; + } Section { height: auto; min-width: 40; - margin: 2 2; + margin: 1 2 4 2; + } SectionTitle { @@ -145,13 +147,46 @@ QuickAccess { LocationLink { margin: 1 0 0 1; height: 1; - padding: 0 2; - background: $boost; - border: tall $surface; - text-align: center; + padding: 1 2; + background: $boost; + color: $text; + + content-align: center middle; } LocationLink:hover { - background: $primary 20%; + background: $accent; color: $text; + text-style: bold; +} + + +.pad { + margin: 1 0; +} + +DataTable { + height: 10; +} + +LoginForm { + height: auto; + margin: 1 0; + padding: 1 2; + layout: grid; + grid-size: 2; + grid-rows: 4; + grid-columns: 12 1fr; + background: $boost; + border: wide $background; +} + +LoginForm Button{ + margin: 0 1; + width: 100%; +} + +LoginForm .label { + padding: 1 2; + text-align: right; } diff --git a/src/textual/demo.py b/src/textual/demo.py index ec22224af..721a7664d 100644 --- a/src/textual/demo.py +++ b/src/textual/demo.py @@ -1,13 +1,73 @@ +from rich import box from rich.console import RenderableType from rich.markdown import Markdown from rich.syntax import Syntax +from rich.pretty import Pretty +from rich.table import Table from rich.text import Text from textual.app import App, ComposeResult - +from textual.binding import Binding from textual.containers import Container, Horizontal from textual.reactive import reactive, watch -from textual.widgets import Header, Footer, Static, Button, Checkbox, TextLog +from textual.widgets import ( + DataTable, + Header, + Footer, + Static, + Button, + Checkbox, + TextLog, + Input, +) + +from_markup = Text.from_markup + +example_table = Table( + show_edge=False, + show_header=True, + expand=True, + row_styles=["none", "dim"], + box=box.SIMPLE, +) +example_table.add_column(from_markup("[green]Date"), style="green", no_wrap=True) +example_table.add_column(from_markup("[blue]Title"), style="blue") +example_table.add_column( + from_markup("[cyan]Production Budget"), + style="cyan", + justify="right", + no_wrap=True, +) +example_table.add_column( + from_markup("[magenta]Box Office"), + style="magenta", + justify="right", + no_wrap=True, +) +example_table.add_row( + "Dec 20, 2019", + "Star Wars: The Rise of Skywalker", + "$275,000,000", + "$375,126,118", +) +example_table.add_row( + "May 25, 2018", + from_markup("[b]Solo[/]: A Star Wars Story"), + "$275,000,000", + "$393,151,347", +) +example_table.add_row( + "Dec 15, 2017", + "Star Wars Ep. VIII: The Last Jedi", + "$262,000,000", + from_markup("[bold]$1,332,539,889[/bold]"), +) +example_table.add_row( + "May 19, 1999", + from_markup("Star Wars Ep. [b]I[/b]: [i]The phantom Menace"), + "$115,000,000", + "$1,027,044,677", +) WELCOME_MD = """ @@ -18,7 +78,17 @@ Textual is a framework for creating sophisticated applications with the terminal Powered by **Rich** -GITHUB: https://github.com/Textualize/textual +""" + + +RICH_MD = """ + +Textual is built on Rich, one of the most popular libraries for Python. + +Use any Rich *renderable* to add content to a Textual App (this text is rendered with Markdown). + +Here are some examples: + """ @@ -51,6 +121,26 @@ Sidebar.-hidden { offset-x: -100%; }""" +DATA = { + "foo": [ + 3.1427, + ( + "Paul Atreides", + "Vladimir Harkonnen", + "Thufir Hawat", + "Gurney Halleck" "Duncan Idaho", + ), + ], +} + +WIDGETS_MD = """ + +Textual widgets are powerful self-container components. + +Build your own or use the builtin widgets. + +""" + class Body(Container): pass @@ -86,7 +176,7 @@ class Welcome(Container): def on_button_pressed(self, event: Button.Pressed) -> None: self.app.add_note("[b magenta]Start!") - self.app.query_one(".location-first").scroll_visible(speed=50) + self.app.query_one(".location-first").scroll_visible(speed=50, top=True) class OptionGroup(Container): @@ -97,24 +187,10 @@ class SectionTitle(Static): pass -class Option(Static): - def __init__(self, label: str, reveal: str) -> None: - super().__init__(label) - self.reveal = reveal - - def on_click(self) -> None: - self.app.query_one(self.reveal).scroll_visible() - self.app.add_note(f"Scrolling to [b]{self.reveal}[/b]") - - class Sidebar(Container): def compose(self) -> ComposeResult: yield Title("[@click=open_docs]Textual Demo[/]") - yield OptionGroup( - Option("TOP", ".location-top"), - Option("First", ".location-first"), - Option("Baz", ""), - ) + yield OptionGroup() yield DarkSwitch() @@ -145,17 +221,28 @@ class LocationLink(Static): self.reveal = reveal def on_click(self) -> None: - self.app.query_one(self.reveal).scroll_visible() + self.app.query_one(self.reveal).scroll_visible(top=True) self.app.add_note(f"Scrolling to [b]{self.reveal}[/b]") +class LoginForm(Container): + def compose(self) -> ComposeResult: + yield Static("Username", classes="label") + yield Input(placeholder="Username") + yield Static("Password", classes="label") + yield Input(placeholder="Password", password=True) + yield Static() + yield Button("Login", variant="primary") + + class DemoApp(App): CSS_PATH = "demo.css" TITLE = "Textual Demo" BINDINGS = [ - ("s", "app.toggle_class('Sidebar', '-hidden')", "Sidebar"), - ("d", "app.toggle_dark", "Toggle Dark mode"), - ("n", "app.toggle_class('TextLog', '-hidden')", "Notes"), + ("ctrl+s", "app.toggle_class('Sidebar', '-hidden')", "Sidebar"), + ("ctrl+d", "app.toggle_dark", "Toggle Dark mode"), + ("f1", "app.toggle_class('TextLog', '-hidden')", "Notes"), + Binding("ctrl+c,ctrl+q", "app.quit", "Quit", show=True), ] show_sidebar = reactive(False) @@ -174,10 +261,20 @@ class DemoApp(App): Body( QuickAccess( LocationLink("TOP", ".location-top"), - LocationLink("First", ".location-first"), - LocationLink("Baz", ""), + LocationLink("Rich", ".location-rich"), + LocationLink("CSS", ".location-css"), + LocationLink("Widgets", ".location-widgets"), ), AboveFold(Welcome(), classes="location-top"), + Column( + Section( + SectionTitle("Rich"), + Text(Markdown(RICH_MD)), + Static(Pretty(DATA, indent_guides=True), classes="pretty pad"), + Static(example_table, classes="table pad"), + ), + classes="location-rich location-first", + ), Column( Section( SectionTitle("CSS"), @@ -188,12 +285,33 @@ class DemoApp(App): ) ), ), - classes="location-first", + classes="location-css", + ), + Column( + Section( + SectionTitle("Widgets"), + Text(Markdown(WIDGETS_MD)), + LoginForm(), + DataTable(), + ), + classes="location-widgets", ), ), ) yield Footer() + def on_mount(self) -> None: + table = self.query_one(DataTable) + table.add_column("Foo", width=20) + table.add_column("Bar", width=20) + table.add_column("Baz", width=20) + table.add_column("Foo", width=20) + table.add_column("Bar", width=20) + table.add_column("Baz", width=20) + table.zebra_stripes = True + for n in range(20): + table.add_row(*[f"Cell ([b]{n}[/b], {col})" for col in range(6)]) + app = DemoApp() if __name__ == "__main__": diff --git a/src/textual/geometry.py b/src/textual/geometry.py index d5e356766..55ad94ef7 100644 --- a/src/textual/geometry.py +++ b/src/textual/geometry.py @@ -307,7 +307,9 @@ class Region(NamedTuple): return cls(x, y, width, height) @classmethod - def get_scroll_to_visible(cls, window_region: Region, region: Region) -> Offset: + def get_scroll_to_visible( + cls, window_region: Region, region: Region, *, top: bool = False + ) -> Offset: """Calculate the smallest offset required to translate a window so that it contains another region. @@ -316,6 +318,7 @@ class Region(NamedTuple): Args: window_region (Region): The window region. region (Region): The region to move inside the window. + top (bool, optional): Get offset to top of window. Defaults to False Returns: Offset: An offset required to add to region to move it inside window_region. @@ -327,7 +330,7 @@ class Region(NamedTuple): window_left, window_top, window_right, window_bottom = window_region.corners region = region.crop_size(window_region.size) - left, top, right, bottom = region.corners + left, top_, right, bottom = region.corners delta_x = delta_y = 0 if not ( @@ -343,15 +346,18 @@ class Region(NamedTuple): ) if not ( - (window_bottom > top >= window_top) + (window_bottom > top_ >= window_top) and (window_bottom > bottom >= window_top) ): # The window needs to scroll on the Y axis to bring region in to view - delta_y = min( - top - window_top, - top - (window_bottom - region.height), - key=abs, - ) + if top: + delta_y = top_ - window_top + else: + delta_y = min( + top_ - window_top, + top_ - (window_bottom - region.height), + key=abs, + ) return Offset(delta_x, delta_y) def __bool__(self) -> bool: diff --git a/src/textual/widget.py b/src/textual/widget.py index 5b73d2063..e3c18fc45 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -1345,6 +1345,7 @@ class Widget(DOMNode): animate: bool = True, speed: float | None = None, duration: float | None = None, + top: bool = False, ) -> bool: """Scroll scrolling to bring a widget in to view. @@ -1370,6 +1371,7 @@ class Widget(DOMNode): animate=animate, speed=speed, duration=duration, + top=top, ) if scroll_offset: scrolled = True @@ -1396,6 +1398,7 @@ class Widget(DOMNode): animate: bool = True, speed: float | None = None, duration: float | None = None, + top: bool = False, ) -> Offset: """Scrolls a given region in to view, if required. @@ -1408,6 +1411,7 @@ class Widget(DOMNode): animate (bool, optional): True to animate, or False to jump. Defaults to True. speed (float | None, optional): Speed of scroll if animate is True. Or None to use duration. duration (float | None, optional): Duration of animation, if animate is True and speed is None. + top (bool, optional): Scroll region to top of container. Defaults to False. Returns: Offset: The distance that was scrolled. @@ -1419,7 +1423,7 @@ class Widget(DOMNode): if window in region: return Offset() - delta_x, delta_y = Region.get_scroll_to_visible(window, region) + delta_x, delta_y = Region.get_scroll_to_visible(window, region, top=top) scroll_x, scroll_y = self.scroll_offset delta = Offset( clamp(scroll_x + delta_x, 0, self.max_scroll_x) - scroll_x, @@ -1440,8 +1444,10 @@ class Widget(DOMNode): def scroll_visible( self, animate: bool = True, + *, speed: float | None = None, duration: float | None = None, + top: bool = False, ) -> None: """Scroll the container to make this widget visible. @@ -1449,6 +1455,7 @@ class Widget(DOMNode): animate (bool, optional): _description_. Defaults to True. speed (float | None, optional): _description_. Defaults to None. duration (float | None, optional): _description_. Defaults to None. + top (bool, optional): Scroll to top of container. Defaults to False. """ parent = self.parent if isinstance(parent, Widget): @@ -1458,6 +1465,7 @@ class Widget(DOMNode): animate=animate, speed=speed, duration=duration, + top=top, ) def __init_subclass__( diff --git a/src/textual/widgets/_header.py b/src/textual/widgets/_header.py index d22c04c89..c4382906d 100644 --- a/src/textual/widgets/_header.py +++ b/src/textual/widgets/_header.py @@ -89,9 +89,9 @@ class Header(Widget): } """ - tall = Reactive(True) + tall = Reactive(False) - DEFAULT_CLASSES = "-tall" + DEFAULT_CLASSES = "" def __init__( self, From a790910d7a6fbebc1852faf99e24688cc99ab84f Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 19 Oct 2022 22:02:58 +0100 Subject: [PATCH 04/17] demo update --- src/textual/demo.css | 17 +++++++++++-- src/textual/demo.py | 58 ++++++++++++++++++++++++++++++++------------ 2 files changed, 57 insertions(+), 18 deletions(-) diff --git a/src/textual/demo.css b/src/textual/demo.css index 14680492d..f9b8c00f6 100644 --- a/src/textual/demo.css +++ b/src/textual/demo.css @@ -19,11 +19,12 @@ Sidebar.-hidden { Sidebar Title { background: $boost; - color: $text; + color: $secondary; padding: 2 4; border-right: vkey $background; dock: top; text-align: center; + text-style: bold; } @@ -79,6 +80,7 @@ Welcome Button { Column { height: auto; + min-height: 100vh; align: center top; } @@ -97,6 +99,10 @@ DarkSwitch .label { color: $text-muted; } +DarkSwitch Checkbox { + background: $boost; +} + Screen > Container { height: 100%; @@ -112,7 +118,7 @@ TextLog { border-top: hkey $primary; offset-y: 0; transition: offset 400ms in_out_cubic; - padding: 0 1; + padding: 0 1 1 1; } TextLog.-hidden { @@ -134,6 +140,8 @@ SectionTitle { text-style: bold; } + + Text { margin: 1 0; } @@ -190,3 +198,8 @@ LoginForm .label { padding: 1 2; text-align: right; } + +Message { + margin: 0 1; + +} diff --git a/src/textual/demo.py b/src/textual/demo.py index 721a7664d..3f4945901 100644 --- a/src/textual/demo.py +++ b/src/textual/demo.py @@ -76,8 +76,6 @@ WELCOME_MD = """ Textual is a framework for creating sophisticated applications with the terminal. -Powered by **Rich** - """ @@ -135,10 +133,35 @@ DATA = { WIDGETS_MD = """ -Textual widgets are powerful self-container components. +Textual widgets are powerful interactive components. Build your own or use the builtin widgets. +- **Input** Text / Password input. +- **Button** Clickable button with a number of styles. +- **Checkbox** A checkbox to toggle between states. +- **DataTable** A spreadsheet-like widget for navigating data. Cells may contain text or Rich renderables. +- **TreeControl** An generic tree with expandable nodes. +- **DirectoryTree** A tree of file and folders. +- *... many more planned ...* + +""" + + +MESSAGE = """ +We hope you enjoy using Textual. + +Here are some links. You can click these! + +[@click="app.open_link('https://textual.textualize.io')"]Textual Docs[/] + +[@click="app.open_link('https://github.com/Textualize/textual')"]Textual GitHub Repository[/] + +[@click="app.open_link('https://github.com/Textualize/rich')"]Rich GitHub Repository[/] + + +Built with ♥ by [@click="app.open_link(https://www.textualize.io)"]Textualize.io[/] + """ @@ -147,11 +170,7 @@ class Body(Container): class Title(Static): - def action_open_docs(self) -> None: - self.app.bell() - import webbrowser - - webbrowser.open("https://textual.textualize.io") + pass class DarkSwitch(Horizontal): @@ -163,7 +182,7 @@ class DarkSwitch(Horizontal): watch(self.app, "dark", self.on_dark_change) def on_dark_change(self, dark: bool) -> None: - self.query_one(Checkbox).value = dark + self.query_one(Checkbox).value = self.app.dark def on_checkbox_changed(self, event: Checkbox.Changed) -> None: self.app.dark = event.value @@ -187,11 +206,14 @@ class SectionTitle(Static): pass +class Message(Static): + pass + + class Sidebar(Container): def compose(self) -> ComposeResult: - yield Title("[@click=open_docs]Textual Demo[/]") - yield OptionGroup() - + yield Title("Textual Demo") + yield OptionGroup(Message(MESSAGE)) yield DarkSwitch() @@ -240,7 +262,7 @@ class DemoApp(App): TITLE = "Textual Demo" BINDINGS = [ ("ctrl+s", "app.toggle_class('Sidebar', '-hidden')", "Sidebar"), - ("ctrl+d", "app.toggle_dark", "Toggle Dark mode"), + ("ctrl+t", "app.toggle_dark", "Toggle Dark mode"), ("f1", "app.toggle_class('TextLog', '-hidden')", "Notes"), Binding("ctrl+c,ctrl+q", "app.quit", "Quit", show=True), ] @@ -250,9 +272,6 @@ class DemoApp(App): def add_note(self, renderable: RenderableType) -> None: self.query_one(TextLog).write(renderable) - def on_mount(self) -> None: - self.add_note("Textual Demo app is running") - def compose(self) -> ComposeResult: yield Container( Sidebar(classes="-hidden"), @@ -300,7 +319,14 @@ class DemoApp(App): ) yield Footer() + def action_open_link(self, link: str) -> None: + self.app.bell() + import webbrowser + + webbrowser.open(link) + def on_mount(self) -> None: + self.add_note("Textual Demo app is running") table = self.query_one(DataTable) table.add_column("Foo", width=20) table.add_column("Bar", width=20) From 24b39fc5dd0b67380bbad965cab0c681cfd5414b Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 20 Oct 2022 11:23:58 +0100 Subject: [PATCH 05/17] demo update --- src/textual/demo.css | 23 +++++++++++++++++- src/textual/demo.py | 55 +++++++++++++++++++++++++++++++++----------- 2 files changed, 64 insertions(+), 14 deletions(-) diff --git a/src/textual/demo.css b/src/textual/demo.css index f9b8c00f6..5e4a9b391 100644 --- a/src/textual/demo.css +++ b/src/textual/demo.css @@ -140,7 +140,12 @@ SectionTitle { text-style: bold; } - +SubTitle { + padding-top: 1; + border-bottom: heavy $panel; + color: $text; + text-style: bold; +} Text { margin: 1 0; @@ -203,3 +208,19 @@ Message { margin: 0 1; } + + +TreeControl { + margin: 1 0; +} + + +Window { + overflow: auto; + height: auto; + max-height: 16; +} + +Window > Static { + width: auto; +} diff --git a/src/textual/demo.py b/src/textual/demo.py index 3f4945901..23ef5217e 100644 --- a/src/textual/demo.py +++ b/src/textual/demo.py @@ -5,6 +5,7 @@ from rich.syntax import Syntax from rich.pretty import Pretty from rich.table import Table from rich.text import Text +from rich.json import JSON from textual.app import App, ComposeResult from textual.binding import Binding @@ -32,12 +33,7 @@ example_table = Table( ) example_table.add_column(from_markup("[green]Date"), style="green", no_wrap=True) example_table.add_column(from_markup("[blue]Title"), style="blue") -example_table.add_column( - from_markup("[cyan]Production Budget"), - style="cyan", - justify="right", - no_wrap=True, -) + example_table.add_column( from_markup("[magenta]Box Office"), style="magenta", @@ -47,25 +43,21 @@ example_table.add_column( example_table.add_row( "Dec 20, 2019", "Star Wars: The Rise of Skywalker", - "$275,000,000", "$375,126,118", ) example_table.add_row( "May 25, 2018", from_markup("[b]Solo[/]: A Star Wars Story"), - "$275,000,000", "$393,151,347", ) example_table.add_row( "Dec 15, 2017", "Star Wars Ep. VIII: The Last Jedi", - "$262,000,000", from_markup("[bold]$1,332,539,889[/bold]"), ) example_table.add_row( "May 19, 1999", from_markup("Star Wars Ep. [b]I[/b]: [i]The phantom Menace"), - "$115,000,000", "$1,027,044,677", ) @@ -74,16 +66,16 @@ WELCOME_MD = """ ## Textual Demo -Textual is a framework for creating sophisticated applications with the terminal. +**Welcome**! Textual is a framework for creating sophisticated applications with the terminal. """ RICH_MD = """ -Textual is built on Rich, one of the most popular libraries for Python. +Textual is built on **Rich**, the popular Python library for advanced terminal output. -Use any Rich *renderable* to add content to a Textual App (this text is rendered with Markdown). +Add content to your Textual App with Rich *renderables* (this text is written in Markdown and formatted with Rich's Markdown class). Here are some examples: @@ -165,6 +157,31 @@ Built with ♥ by [@click="app.open_link(https://www.textualize.io)"]Textualize """ +JSON_EXAMPLE = """{ + "glossary": { + "title": "example glossary", + "GlossDiv": { + "title": "S", + "GlossList": { + "GlossEntry": { + "ID": "SGML", + "SortAs": "SGML", + "GlossTerm": "Standard Generalized Markup Language", + "Acronym": "SGML", + "Abbrev": "ISO 8879:1986", + "GlossDef": { + "para": "A meta-markup language, used to create markup languages such as DocBook.", + "GlossSeeAlso": ["GML", "XML"] + }, + "GlossSee": "markup" + } + } + } + } +} +""" + + class Body(Container): pass @@ -257,6 +274,14 @@ class LoginForm(Container): yield Button("Login", variant="primary") +class Window(Container): + pass + + +class SubTitle(Static): + pass + + class DemoApp(App): CSS_PATH = "demo.css" TITLE = "Textual Demo" @@ -289,8 +314,12 @@ class DemoApp(App): Section( SectionTitle("Rich"), Text(Markdown(RICH_MD)), + SubTitle("Pretty Printed data (try resizing the terminal)"), Static(Pretty(DATA, indent_guides=True), classes="pretty pad"), + SubTitle("Tables"), Static(example_table, classes="table pad"), + SubTitle("JSON"), + Window(Static(JSON(JSON_EXAMPLE), expand=True, classes="pad")), ), classes="location-rich location-first", ), From 4f4696e832adcf25464495fa2c409839f113224b Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 20 Oct 2022 14:55:07 +0100 Subject: [PATCH 06/17] docs and examples polish --- docs/getting_started.md | 49 +++++++++++++++++++++++++++++++++++++---- docs/tutorial.md | 4 ++-- src/textual/demo.py | 29 ++++++++++++------------ 3 files changed, 62 insertions(+), 20 deletions(-) diff --git a/docs/getting_started.md b/docs/getting_started.md index f9446456a..d41751bd3 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -25,18 +25,59 @@ You can install Textual via PyPI. If you plan on developing Textual apps, then you should install `textual[dev]`. The `[dev]` part installs a few extra dependencies for development. ``` -pip install "textual[dev]==0.2.0b7" +pip install "textual[dev]" ``` If you only plan on _running_ Textual apps, then you can drop the `[dev]` part: ``` -pip install textual==0.2.0b7 +pip install textual ``` -!!! important +## Demo + +Once you have Textual installed, run the following to get an impression of what it can do: + +```bash +python -m textual +``` + +If Textual is installed you should see the following: + +```{.textual path="src/textual/demo.py" columns="127" lines="53" press="enter,_,_,_,_,_,_,tab,w,i,l,l"} +``` + +## Examples + + +The Textual repository comes with a number of example apps. To try out the examples, first clone the Textual repository: + +=== "HTTPS" + + ```bash + git clone https://github.com/Textualize/textual.git + ``` + +=== "SSH" + + ```bash + git clone git@github.com:Textualize/textual.git + ``` + +=== "GitHub CLI" + + ```bash + gh repo clone Textualize/textual + ``` + + +With the repository cloned, navigate to the `/examples/` directory where you fill find a number of Python files you can run from the command line: + +```bash +cd textual/examples/ +python code_browser.py ../ +``` - There may be a more recent beta version since the time of writing. Check the [release history](https://pypi.org/project/textual/#history) for a more recent version. ## Textual CLI diff --git a/docs/tutorial.md b/docs/tutorial.md index 52d12df70..ed1ab61bc 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -35,13 +35,13 @@ If you want to try the finished Stopwatch app and follow along with the code, fi === "HTTPS" ```bash - git clone -b css https://github.com/Textualize/textual.git + git clone https://github.com/Textualize/textual.git ``` === "SSH" ```bash - git clone -b css git@github.com:Textualize/textual.git + git clone git@github.com:Textualize/textual.git ``` === "GitHub CLI" diff --git a/src/textual/demo.py b/src/textual/demo.py index 7c11d24af..6f39635ec 100644 --- a/src/textual/demo.py +++ b/src/textual/demo.py @@ -119,7 +119,8 @@ DATA = { "Paul Atreides", "Vladimir Harkonnen", "Thufir Hawat", - "Gurney Halleck" "Duncan Idaho", + "Gurney Halleck", + "Duncan Idaho", ), ], } @@ -307,23 +308,32 @@ class DemoApp(App): Body( QuickAccess( LocationLink("TOP", ".location-top"), + LocationLink("Widgets", ".location-widgets"), LocationLink("Rich content", ".location-rich"), LocationLink("CSS", ".location-css"), - LocationLink("Widgets", ".location-widgets"), ), AboveFold(Welcome(), classes="location-top"), + Column( + Section( + SectionTitle("Widgets"), + Text(Markdown(WIDGETS_MD)), + LoginForm(), + DataTable(), + ), + classes="location-widgets location-first", + ), Column( Section( SectionTitle("Rich"), Text(Markdown(RICH_MD)), SubTitle("Pretty Printed data (try resizing the terminal)"), Static(Pretty(DATA, indent_guides=True), classes="pretty pad"), - SubTitle("Tables"), - Static(example_table, classes="table pad"), SubTitle("JSON"), Window(Static(JSON(JSON_EXAMPLE), expand=True), classes="pad"), + SubTitle("Tables"), + Static(example_table, classes="table pad"), ), - classes="location-rich location-first", + classes="location-rich", ), Column( Section( @@ -337,15 +347,6 @@ class DemoApp(App): ), classes="location-css", ), - Column( - Section( - SectionTitle("Widgets"), - Text(Markdown(WIDGETS_MD)), - LoginForm(), - DataTable(), - ), - classes="location-widgets", - ), ), ) yield Footer() From 58962d293a21e08d9b6fa842a26e91633fa8b2f3 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 20 Oct 2022 14:56:58 +0100 Subject: [PATCH 07/17] move app --- src/textual/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/__main__.py b/src/textual/__main__.py index 46d0f0c7e..de8c1b79f 100644 --- a/src/textual/__main__.py +++ b/src/textual/__main__.py @@ -1,6 +1,6 @@ from .demo import DemoApp -app = DemoApp() if __name__ == "__main__": + app = DemoApp() app.run() From c0a0bf8ed5416a6b9c74d3334b7c84bbd02f3fdd Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 20 Oct 2022 15:48:43 +0100 Subject: [PATCH 08/17] links --- src/textual/demo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/textual/demo.py b/src/textual/demo.py index 6f39635ec..ae3d955c1 100644 --- a/src/textual/demo.py +++ b/src/textual/demo.py @@ -73,7 +73,7 @@ WELCOME_MD = """ RICH_MD = """ -Textual is built on **Rich**, the popular Python library for advanced terminal output. +Textual is built on [Rich](https://github.com/Textualize/rich), the popular Python library for advanced terminal output. Add content to your Textual App with Rich *renderables* (this text is written in Markdown and formatted with Rich's Markdown class). @@ -137,7 +137,7 @@ Build your own or use the builtin widgets. - **DataTable** A spreadsheet-like widget for navigating data. Cells may contain text or Rich renderables. - **TreeControl** An generic tree with expandable nodes. - **DirectoryTree** A tree of file and folders. -- *... many more planned ...* +- *... [many more planned](https://textual.textualize.io/roadmap/) ...* """ From af5a257660e203765edb387d70c157297b2e4389 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 20 Oct 2022 17:54:56 +0100 Subject: [PATCH 09/17] focus within issues --- src/textual/css/styles.py | 3 ++- src/textual/css/stylesheet.py | 3 +++ src/textual/demo.css | 16 +++++++++++++--- src/textual/demo.py | 14 +++++++++++++- src/textual/dom.py | 1 + src/textual/widget.py | 23 +++++++---------------- 6 files changed, 39 insertions(+), 21 deletions(-) diff --git a/src/textual/css/styles.py b/src/textual/css/styles.py index cbb99b509..b4809f716 100644 --- a/src/textual/css/styles.py +++ b/src/textual/css/styles.py @@ -610,7 +610,8 @@ class Styles(StylesBase): list[tuple[str, Specificity6, Any]]]: A list containing a tuple of , . """ is_important = self.important.__contains__ - + if self.important: + print(self._rules) rules = [ ( rule_name, diff --git a/src/textual/css/stylesheet.py b/src/textual/css/stylesheet.py index 13475f992..9d09835c6 100644 --- a/src/textual/css/stylesheet.py +++ b/src/textual/css/stylesheet.py @@ -370,11 +370,14 @@ class Stylesheet: # Collect the rules defined in the stylesheet node._has_hover_style = False + node._has_focus_within = False for rule in rules: is_default_rules = rule.is_default_rules tie_breaker = rule.tie_breaker if ":hover" in rule.selector_names: node._has_hover_style = True + if ":focus-within" in rule.selector_names: + node._has_focus_within = True for base_specificity in _check_rule(rule, css_path_nodes): for key, rule_specificity, value in rule.styles.extract_rules( base_specificity, is_default_rules, tie_breaker diff --git a/src/textual/demo.css b/src/textual/demo.css index b60e54cb7..363f13b75 100644 --- a/src/textual/demo.css +++ b/src/textual/demo.css @@ -16,6 +16,10 @@ Sidebar { } +Sidebar:focus-within { + offset: 0 0 !important; +} + Sidebar.-hidden { offset-x: -100%; } @@ -125,11 +129,17 @@ TextLog { padding: 0 1 1 1; } -TextLog.-hidden { - offset-y: 100%; - + +TextLog:focus { + offset: 0 0 !important; } +TextLog.-hidden { + offset-y: 100%; +} + + + Section { height: auto; min-width: 40; diff --git a/src/textual/demo.py b/src/textual/demo.py index ae3d955c1..4f9f52547 100644 --- a/src/textual/demo.py +++ b/src/textual/demo.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from rich import box from rich.console import RenderableType from rich.markdown import Markdown @@ -288,7 +290,7 @@ class DemoApp(App): CSS_PATH = "demo.css" TITLE = "Textual Demo" BINDINGS = [ - ("ctrl+b", "app.toggle_class('Sidebar', '-hidden')", "Sidebar"), + ("ctrl+b", "toggle_sidebar", "Sidebar"), ("ctrl+t", "app.toggle_dark", "Toggle Dark mode"), ("ctrl+s", "app.screenshot()", "Screenshot"), ("f1", "app.toggle_class('TextLog', '-hidden')", "Notes"), @@ -357,6 +359,16 @@ class DemoApp(App): webbrowser.open(link) + def action_toggle_sidebar(self) -> None: + self.bell() + sidebar = self.query_one(Sidebar) + if sidebar.has_class("-hidden"): + sidebar.remove_class("-hidden") + else: + if sidebar.query("*:focus"): + self.screen.set_focus(None) + sidebar.add_class("-hidden") + def on_mount(self) -> None: self.add_note("Textual Demo app is running") table = self.query_one(DataTable) diff --git a/src/textual/dom.py b/src/textual/dom.py index af48bf1df..06c95d65d 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -140,6 +140,7 @@ class DOMNode(MessagePump): self._css_types = {cls.__name__ for cls in self._css_bases(self.__class__)} self._bindings = Bindings(self.BINDINGS) self._has_hover_style: bool = False + self._has_focus_within: bool = False super().__init__() diff --git a/src/textual/widget.py b/src/textual/widget.py index 25a565aef..32d255ae6 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -238,7 +238,6 @@ class Widget(DOMNode): auto_width = Reactive(True) auto_height = Reactive(True) has_focus = Reactive(False) - descendant_has_focus = Reactive(False) mouse_over = Reactive(False) scroll_x = Reactive(0.0, repaint=False, layout=False) scroll_y = Reactive(0.0, repaint=False, layout=False) @@ -1577,7 +1576,8 @@ class Widget(DOMNode): yield "hover" if self.has_focus: yield "focus" - if self.descendant_has_focus: + focused = self.screen.focused + if focused and self in focused.ancestors: yield "focus-within" def post_render(self, renderable: RenderableType) -> ConsoleRenderable: @@ -1927,27 +1927,18 @@ class Widget(DOMNode): self.mouse_over = True def _on_focus(self, event: events.Focus) -> None: - self.emit_no_wait(events.DescendantFocus(self)) + for node in self.ancestors: + if node._has_focus_within: + self.app.update_styles(node) self.has_focus = True self.refresh() def _on_blur(self, event: events.Blur) -> None: - self.emit_no_wait(events.DescendantBlur(self)) + if any(node._has_focus_within for node in self.ancestors): + self.app.update_styles(self) self.has_focus = False self.refresh() - def _on_descendant_focus(self, event: events.DescendantFocus) -> None: - if not self.descendant_has_focus: - self.descendant_has_focus = True - - def _on_descendant_blur(self, event: events.DescendantBlur) -> None: - if self.descendant_has_focus: - self.descendant_has_focus = False - - def watch_descendant_has_focus(self, value: bool) -> None: - if "focus-within" in self.pseudo_classes: - self.app._require_stylesheet_update.add(self) - def _on_mouse_scroll_down(self, event) -> None: if self.allow_vertical_scroll: if self.scroll_down(animate=False): From e419a0821cda7c5860c171a20c09034daaf82dea Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 20 Oct 2022 19:36:36 +0100 Subject: [PATCH 10/17] fix for focus reset edge case --- src/textual/demo.css | 1 + src/textual/demo.py | 15 ++++++++++++--- src/textual/dom.py | 2 +- src/textual/screen.py | 17 ++++++++++++----- src/textual/widget.py | 21 ++++++++++++++++++--- 5 files changed, 44 insertions(+), 12 deletions(-) diff --git a/src/textual/demo.css b/src/textual/demo.css index 363f13b75..a6ba994ba 100644 --- a/src/textual/demo.css +++ b/src/textual/demo.css @@ -196,6 +196,7 @@ DataTable { height: 16; } + LoginForm { height: auto; margin: 1 0; diff --git a/src/textual/demo.py b/src/textual/demo.py index 4f9f52547..07f2dea09 100644 --- a/src/textual/demo.py +++ b/src/textual/demo.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + from rich import box from rich.console import RenderableType from rich.markdown import Markdown @@ -303,6 +305,7 @@ class DemoApp(App): self.query_one(TextLog).write(renderable) def compose(self) -> ComposeResult: + example_css = "\n".join(Path(self.css_path).read_text().splitlines()[:50]) yield Container( Sidebar(classes="-hidden"), Header(show_clock=True), @@ -341,9 +344,15 @@ class DemoApp(App): Section( SectionTitle("CSS"), Text(Markdown(CSS_MD)), - Static( - Syntax( - EXAMPLE_CSS, "css", theme="material", line_numbers=True + Window( + Static( + Syntax( + example_css, + "css", + theme="material", + line_numbers=True, + ), + expand=True, ) ), ), diff --git a/src/textual/dom.py b/src/textual/dom.py index 06c95d65d..1cfc522b9 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -278,7 +278,7 @@ class DOMNode(MessagePump): while node and not isinstance(node, Screen): node = node._parent if not isinstance(node, Screen): - raise NoScreen(f"{self} has no screen") + raise NoScreen(f"node has no screen") return node @property diff --git a/src/textual/screen.py b/src/textual/screen.py index 2f110c2ff..70b971125 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -225,7 +225,7 @@ class Screen(Widget): return self._move_focus(-1) def _reset_focus( - self, widget: Widget, avoiding: list[DOMNode] | None = None + self, widget: Widget, avoiding: list[Widget] | None = None ) -> None: """Reset the focus when a widget is removed @@ -252,14 +252,20 @@ class Screen(Widget): # the focus chain. widget_index = focusable_widgets.index(widget) except ValueError: - # Seems we can't find it. There's no good reason this should - # happen but, on the off-chance, let's go into a "no focus" state. - self.set_focus(None) + # widget is not in focusable widgets + # It may have been made invisible + # Move to a sibling if possible + for sibling in widget.visible_siblings: + if sibling not in avoiding and sibling.can_focus: + self.set_focus(sibling) + break + else: + self.set_focus(None) return # Now go looking for something before it, that isn't about to be # removed, and which can receive focus, and go focus that. - chosen: DOMNode | None = None + chosen: Widget | None = None for candidate in reversed( focusable_widgets[widget_index + 1 :] + focusable_widgets[:widget_index] ): @@ -368,6 +374,7 @@ class Screen(Widget): hidden, shown, resized = self._compositor.reflow(self, size) Hide = events.Hide Show = events.Show + for widget in hidden: widget.post_message_no_wait(Hide(self)) for widget in shown: diff --git a/src/textual/widget.py b/src/textual/widget.py index 32d255ae6..9f8b75289 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -261,6 +261,18 @@ class Widget(DOMNode): else: return [] + @property + def visible_siblings(self) -> list[Widget]: + """A list of siblings which will be shown. + + Returns: + list[Widget]: List of siblings. + """ + siblings = [ + widget for widget in self.siblings if widget.visible and widget.display + ] + return siblings + @property def allow_vertical_scroll(self) -> bool: """Check if vertical scroll is permitted. @@ -1576,9 +1588,12 @@ class Widget(DOMNode): yield "hover" if self.has_focus: yield "focus" - focused = self.screen.focused - if focused and self in focused.ancestors: - yield "focus-within" + try: + focused = self.screen.focused + if focused and self in focused.ancestors: + yield "focus-within" + except NoScreen: + pass def post_render(self, renderable: RenderableType) -> ConsoleRenderable: """Applies style attributes to the default renderable. From f229d5dd43f7d300816bc33898ad0fab6ec35861 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 20 Oct 2022 19:43:15 +0100 Subject: [PATCH 11/17] removed bell --- src/textual/demo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/demo.py b/src/textual/demo.py index 07f2dea09..d55ea4eb7 100644 --- a/src/textual/demo.py +++ b/src/textual/demo.py @@ -369,7 +369,7 @@ class DemoApp(App): webbrowser.open(link) def action_toggle_sidebar(self) -> None: - self.bell() + sidebar = self.query_one(Sidebar) if sidebar.has_class("-hidden"): sidebar.remove_class("-hidden") From 4a563fe514b8a395c9c58a8b24f250a32f66fe6c Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 20 Oct 2022 19:49:09 +0100 Subject: [PATCH 12/17] scroll to top, add bell --- src/textual/cli/previews/borders.py | 1 + src/textual/cli/previews/colors.css | 3 --- src/textual/cli/previews/colors.py | 3 ++- src/textual/cli/previews/easing.py | 1 + 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/textual/cli/previews/borders.py b/src/textual/cli/previews/borders.py index 55b3cedf6..613343443 100644 --- a/src/textual/cli/previews/borders.py +++ b/src/textual/cli/previews/borders.py @@ -56,6 +56,7 @@ class BorderApp(App): event.button.id, self.stylesheet._variables["secondary"], ) + self.bell() app = BorderApp() diff --git a/src/textual/cli/previews/colors.css b/src/textual/cli/previews/colors.css index f06637893..3af8eabd7 100644 --- a/src/textual/cli/previews/colors.css +++ b/src/textual/cli/previews/colors.css @@ -1,6 +1,3 @@ - * { - transition: color 300ms linear, background 300ms linear; -} ColorButtons { dock: left; diff --git a/src/textual/cli/previews/colors.py b/src/textual/cli/previews/colors.py index 1c5ce0bad..5edd4050e 100644 --- a/src/textual/cli/previews/colors.py +++ b/src/textual/cli/previews/colors.py @@ -78,10 +78,11 @@ class ColorsApp(App): content.mount(ColorsView()) def on_button_pressed(self, event: Button.Pressed) -> None: + self.bell() self.query(ColorGroup).remove_class("-active") group = self.query_one(f"#group-{event.button.id}", ColorGroup) group.add_class("-active") - group.scroll_visible(speed=150) + group.scroll_visible(top=True, speed=150) app = ColorsApp() diff --git a/src/textual/cli/previews/easing.py b/src/textual/cli/previews/easing.py index 795164b6c..6edcdb82a 100644 --- a/src/textual/cli/previews/easing.py +++ b/src/textual/cli/previews/easing.py @@ -84,6 +84,7 @@ class EasingApp(App): ) def on_button_pressed(self, event: Button.Pressed) -> None: + self.bell() self.animated_bar.animation_running = True def _animation_complete(): From bfba0dca09a2fd8532ea2937269ade0826af4b57 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 20 Oct 2022 21:20:30 +0100 Subject: [PATCH 13/17] optimized focus within --- docs/getting_started.md | 10 +++++----- docs/tutorial.md | 6 +++--- pyproject.toml | 2 +- src/textual/css/styles.py | 2 -- src/textual/dom.py | 2 +- src/textual/widget.py | 10 ++++++++-- 6 files changed, 18 insertions(+), 14 deletions(-) diff --git a/docs/getting_started.md b/docs/getting_started.md index d41751bd3..9ee3e138a 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -25,13 +25,13 @@ You can install Textual via PyPI. If you plan on developing Textual apps, then you should install `textual[dev]`. The `[dev]` part installs a few extra dependencies for development. ``` -pip install "textual[dev]" +pip install "textual[dev]==0.2.0b8" ``` If you only plan on _running_ Textual apps, then you can drop the `[dev]` part: ``` -pip install textual +pip install textual==0.2.0b8 ``` ## Demo @@ -55,19 +55,19 @@ The Textual repository comes with a number of example apps. To try out the examp === "HTTPS" ```bash - git clone https://github.com/Textualize/textual.git + git clone -b css https://github.com/Textualize/textual.git ``` === "SSH" ```bash - git clone git@github.com:Textualize/textual.git + git clone -b css git@github.com:Textualize/textual.git ``` === "GitHub CLI" ```bash - gh repo clone Textualize/textual + gh repo clone -b css Textualize/textual ``` diff --git a/docs/tutorial.md b/docs/tutorial.md index ed1ab61bc..0bfa8aa09 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -35,19 +35,19 @@ If you want to try the finished Stopwatch app and follow along with the code, fi === "HTTPS" ```bash - git clone https://github.com/Textualize/textual.git + git clone -b css https://github.com/Textualize/textual.git ``` === "SSH" ```bash - git clone git@github.com:Textualize/textual.git + git clone -b css git@github.com:Textualize/textual.git ``` === "GitHub CLI" ```bash - gh repo clone Textualize/textual + gh repo clone -b css Textualize/textual ``` diff --git a/pyproject.toml b/pyproject.toml index 378015988..cc3482a85 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "textual" -version = "0.2.0b7" +version = "0.2.0b8" homepage = "https://github.com/Textualize/textual" description = "Modern Text User Interface framework" authors = ["Will McGugan "] diff --git a/src/textual/css/styles.py b/src/textual/css/styles.py index b4809f716..d830b6774 100644 --- a/src/textual/css/styles.py +++ b/src/textual/css/styles.py @@ -610,8 +610,6 @@ class Styles(StylesBase): list[tuple[str, Specificity6, Any]]]: A list containing a tuple of , . """ is_important = self.important.__contains__ - if self.important: - print(self._rules) rules = [ ( rule_name, diff --git a/src/textual/dom.py b/src/textual/dom.py index 1cfc522b9..1a35b0717 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -278,7 +278,7 @@ class DOMNode(MessagePump): while node and not isinstance(node, Screen): node = node._parent if not isinstance(node, Screen): - raise NoScreen(f"node has no screen") + raise NoScreen("node has no screen") return node @property diff --git a/src/textual/widget.py b/src/textual/widget.py index 9f8b75289..363c1f518 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -1590,10 +1590,16 @@ class Widget(DOMNode): yield "focus" try: focused = self.screen.focused - if focused and self in focused.ancestors: - yield "focus-within" except NoScreen: pass + else: + if focused: + node = focused + while node is not None: + if node is self: + yield "focus-within" + break + node = node._parent def post_render(self, renderable: RenderableType) -> ConsoleRenderable: """Applies style attributes to the default renderable. From 1db932796939e55a14856b65ca00f463fac8861b Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 20 Oct 2022 21:57:31 +0100 Subject: [PATCH 14/17] fix title setting --- examples/five_by_five.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/five_by_five.py b/examples/five_by_five.py index 5d4c71fe3..84a50d639 100644 --- a/examples/five_by_five.py +++ b/examples/five_by_five.py @@ -317,9 +317,8 @@ class FiveByFive(App[None]): #: App-level bindings. BINDINGS = [("ctrl+d", "toggle_dark", "Toggle Dark Mode")] - def __init__(self) -> None: - """Constructor.""" - super().__init__(title="5x5 -- A little annoying puzzle") + # Set the title + TITLE = "5x5 -- A little annoying puzzle" def on_mount(self) -> None: """Set up the application on startup.""" From aba508f68c077c4c372dc2dff9f27666d79fbabc Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 21 Oct 2022 09:29:39 +0100 Subject: [PATCH 15/17] Added notification on save --- src/textual/demo.css | 16 ++++++++++++++-- src/textual/demo.py | 20 +++++++++++++++----- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/textual/demo.css b/src/textual/demo.css index a6ba994ba..260aab7cd 100644 --- a/src/textual/demo.css +++ b/src/textual/demo.css @@ -3,11 +3,23 @@ } Screen { - layers: base overlay notes; + layers: base overlay notes notifications; overflow: hidden; } +Notification { + dock: bottom; + layer: notification; + width: auto; + margin: 2 4; + padding: 1 2; + background: $background; + color: $text; + height: auto; + +} + Sidebar { width: 40; background: $panel; @@ -161,7 +173,7 @@ SubTitle { text-style: bold; } -Text { +TextContent { margin: 1 0; } diff --git a/src/textual/demo.py b/src/textual/demo.py index d55ea4eb7..d79e35e7e 100644 --- a/src/textual/demo.py +++ b/src/textual/demo.py @@ -252,7 +252,7 @@ class Column(Container): pass -class Text(Static): +class TextContent(Static): pass @@ -288,6 +288,14 @@ class SubTitle(Static): pass +class Notification(Static): + def on_mount(self) -> None: + self.set_timer(3, self.remove) + + def on_click(self) -> None: + self.remove() + + class DemoApp(App): CSS_PATH = "demo.css" TITLE = "Textual Demo" @@ -321,7 +329,7 @@ class DemoApp(App): Column( Section( SectionTitle("Widgets"), - Text(Markdown(WIDGETS_MD)), + TextContent(Markdown(WIDGETS_MD)), LoginForm(), DataTable(), ), @@ -330,7 +338,7 @@ class DemoApp(App): Column( Section( SectionTitle("Rich"), - Text(Markdown(RICH_MD)), + TextContent(Markdown(RICH_MD)), SubTitle("Pretty Printed data (try resizing the terminal)"), Static(Pretty(DATA, indent_guides=True), classes="pretty pad"), SubTitle("JSON"), @@ -343,7 +351,7 @@ class DemoApp(App): Column( Section( SectionTitle("CSS"), - Text(Markdown(CSS_MD)), + TextContent(Markdown(CSS_MD)), Window( Static( Syntax( @@ -401,7 +409,9 @@ class DemoApp(App): """ self.bell() path = self.save_screenshot(filename, path) - self.add_note(f"Screenshot saved to {path!r}") + message = Text.assemble("Screenshot saved to ", (f"'{path}'", "bold green")) + self.add_note(message) + self.screen.mount(Notification(message)) app = DemoApp() From 4c8bcdaeabc535c76ef502506cc7c33d31f52d5b Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 21 Oct 2022 09:38:51 +0100 Subject: [PATCH 16/17] remove links --- src/textual/demo.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/textual/demo.py b/src/textual/demo.py index d79e35e7e..1c6759bce 100644 --- a/src/textual/demo.py +++ b/src/textual/demo.py @@ -4,26 +4,26 @@ from pathlib import Path from rich import box from rich.console import RenderableType +from rich.json import JSON from rich.markdown import Markdown -from rich.syntax import Syntax from rich.pretty import Pretty +from rich.syntax import Syntax from rich.table import Table from rich.text import Text -from rich.json import JSON from textual.app import App, ComposeResult from textual.binding import Binding from textual.containers import Container, Horizontal from textual.reactive import reactive, watch from textual.widgets import ( - DataTable, - Header, - Footer, - Static, Button, Checkbox, - TextLog, + DataTable, + Footer, + Header, Input, + Static, + TextLog, ) from_markup = Text.from_markup @@ -77,7 +77,7 @@ WELCOME_MD = """ RICH_MD = """ -Textual is built on [Rich](https://github.com/Textualize/rich), the popular Python library for advanced terminal output. +Textual is built on **Rich**, the popular Python library for advanced terminal output. Add content to your Textual App with Rich *renderables* (this text is written in Markdown and formatted with Rich's Markdown class). @@ -141,7 +141,7 @@ Build your own or use the builtin widgets. - **DataTable** A spreadsheet-like widget for navigating data. Cells may contain text or Rich renderables. - **TreeControl** An generic tree with expandable nodes. - **DirectoryTree** A tree of file and folders. -- *... [many more planned](https://textual.textualize.io/roadmap/) ...* +- *... many more planned ...* """ From a9d1499cc76dc77add8df12dfd379feca8f1f451 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 21 Oct 2022 09:43:25 +0100 Subject: [PATCH 17/17] docstring fix --- src/textual/demo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/demo.py b/src/textual/demo.py index 1c6759bce..594e43ab2 100644 --- a/src/textual/demo.py +++ b/src/textual/demo.py @@ -405,7 +405,7 @@ class DemoApp(App): Args: filename (str | None, optional): Filename of screenshot, or None to auto-generate. Defaults to None. - path (str, optional): Path to directory. Defaults to "~/". + path (str, optional): Path to directory. Defaults to "./". """ self.bell() path = self.save_screenshot(filename, path)