diff --git a/docs/examples/app/event01.py b/docs/examples/app/event01.py index 60a411735..0f3bc3cdc 100644 --- a/docs/examples/app/event01.py +++ b/docs/examples/app/event01.py @@ -25,6 +25,6 @@ class EventApp(App): self.screen.styles.background = self.COLORS[int(event.key)] -app = EventApp() if __name__ == "__main__": + app = EventApp() app.run() diff --git a/docs/examples/app/question01.py b/docs/examples/app/question01.py index 01a70c233..e61fba393 100644 --- a/docs/examples/app/question01.py +++ b/docs/examples/app/question01.py @@ -12,7 +12,7 @@ class QuestionApp(App[str]): self.exit(event.button.id) -app = QuestionApp() if __name__ == "__main__": + app = QuestionApp() reply = app.run() print(reply) diff --git a/docs/examples/app/question02.py b/docs/examples/app/question02.py index a1d107c04..36b23722b 100644 --- a/docs/examples/app/question02.py +++ b/docs/examples/app/question02.py @@ -3,6 +3,8 @@ from textual.widgets import Static, Button class QuestionApp(App[str]): + CSS_PATH = "question02.css" + def compose(self) -> ComposeResult: yield Static("Do you love Textual?", id="question") yield Button("Yes", id="yes", variant="primary") @@ -12,7 +14,7 @@ class QuestionApp(App[str]): self.exit(event.button.id) -app = QuestionApp(css_path="question02.css") if __name__ == "__main__": + app = QuestionApp() reply = app.run() print(reply) diff --git a/docs/examples/app/question03.py b/docs/examples/app/question03.py index 1f2f02cd3..41ad0f349 100644 --- a/docs/examples/app/question03.py +++ b/docs/examples/app/question03.py @@ -32,7 +32,7 @@ class QuestionApp(App[str]): self.exit(event.button.id) -app = QuestionApp() if __name__ == "__main__": + app = QuestionApp() reply = app.run() print(reply) diff --git a/docs/examples/app/return.py b/docs/examples/app/return.py index 9e37b4276..d3214318e 100644 --- a/docs/examples/app/return.py +++ b/docs/examples/app/return.py @@ -9,6 +9,6 @@ class ButtonsApp(App): yield Button("Chani") -app = ButtonsApp() if __name__ == "__main__": + app = ButtonsApp() app.run() diff --git a/docs/examples/app/simple02.py b/docs/examples/app/simple02.py index 8b4fec9ce..e087ac2d2 100644 --- a/docs/examples/app/simple02.py +++ b/docs/examples/app/simple02.py @@ -5,6 +5,6 @@ class MyApp(App): pass -app = MyApp() if __name__ == "__main__": + app = MyApp() app.run() diff --git a/docs/examples/app/widgets01.py b/docs/examples/app/widgets01.py index baeb3bec8..c8abd8f48 100644 --- a/docs/examples/app/widgets01.py +++ b/docs/examples/app/widgets01.py @@ -10,6 +10,6 @@ class WelcomeApp(App): self.exit() -app = WelcomeApp() if __name__ == "__main__": + app = WelcomeApp() app.run() diff --git a/docs/examples/app/widgets02.py b/docs/examples/app/widgets02.py index 9fb52c228..43a3bf047 100644 --- a/docs/examples/app/widgets02.py +++ b/docs/examples/app/widgets02.py @@ -10,6 +10,6 @@ class WelcomeApp(App): self.exit() -app = WelcomeApp() if __name__ == "__main__": + app = WelcomeApp() app.run() diff --git a/docs/examples/basic.css b/docs/examples/basic.css deleted file mode 100644 index 825d4e9b6..000000000 --- a/docs/examples/basic.css +++ /dev/null @@ -1,253 +0,0 @@ -/* CSS file for basic.py */ - - - - * { - transition: color 300ms linear, background 300ms linear; -} - - -*:hover { - /* tint: 30% red; - /* outline: heavy red; */ -} - -App > Screen { - - background: $surface; - color: $text; - layers: base sidebar; - - color: $text; - background: $background; - layout: vertical; - - overflow: hidden; - -} - -#tree-container { - overflow-y: auto; - height: 20; - margin: 1 3; - background: $panel; - padding: 1 2; -} - -DirectoryTree { - padding: 0 1; - height: auto; - -} - - - - -DataTable { - /*border:heavy red;*/ - /* tint: 10% green; */ - /* text-opacity: 50%; */ - padding: 1; - margin: 1 2; - height: 24; -} - -#sidebar { - color: $text; - background: $panel; - dock: left; - width: 30; - margin-bottom: 1; - offset-x: -100%; - - transition: offset 500ms in_out_cubic; - layer: sidebar; -} - -#sidebar.-active { - offset-x: 0; -} - -#sidebar .title { - height: 1; - background: $primary-background-darken-1; - color: $text-muted; - border-right: wide $background; - content-align: center middle; -} - -#sidebar .user { - height: 8; - background: $panel-darken-1; - color: $text-muted; - border-right: wide $background; - content-align: center middle; -} - -#sidebar .content { - background: $panel-darken-2; - color: $text; - border-right: wide $background; - content-align: center middle; -} - - - - -Tweet { - height:12; - width: 100%; - margin: 0 2; - - background: $panel; - color: $text; - layout: vertical; - /* border: outer $primary; */ - padding: 1; - border: wide $panel; - overflow: auto; - /* scrollbar-gutter: stable; */ - align-horizontal: center; - box-sizing: border-box; -} - - -.scrollable { - overflow-x: auto; - overflow-y: scroll; - margin: 1 2; - height: 24; - align-horizontal: center; - layout: vertical; -} - -.code { - height: auto; - -} - - -TweetHeader { - height:1; - background: $accent; - color: $text -} - -TweetBody { - width: 100%; - background: $panel; - color: $text; - height: auto; - padding: 0 1 0 0; -} - -Tweet.scroll-horizontal TweetBody { - width: 350; -} - -.button { - background: $accent; - color: $text; - width:20; - height: 3; - /* border-top: hidden $accent-darken-3; */ - border: tall $accent-darken-2; - /* border-left: tall $accent-darken-1; */ - - - /* padding: 1 0 0 0 ; */ - - transition: background 400ms in_out_cubic, color 400ms in_out_cubic; - -} - -.button:hover { - background: $accent-lighten-1; - color: $text-disabled; - width: 20; - height: 3; - border: tall $accent-darken-1; - /* border-left: tall $accent-darken-3; */ - - - - -} - -#footer { - color: $text; - background: $accent; - height: 1; - - content-align: center middle; - dock:bottom; -} - - -#sidebar .content { - layout: vertical -} - -OptionItem { - height: 3; - background: $panel; - border-right: wide $background; - border-left: blank; - content-align: center middle; -} - -OptionItem:hover { - height: 3; - color: $text; - background: $primary-darken-1; - /* border-top: hkey $accent2-darken-3; - border-bottom: hkey $accent2-darken-3; */ - text-style: bold; - border-left: outer $secondary-darken-2; -} - -Error { - width: 100%; - height:3; - background: $error; - color: $text; - border-top: tall $error-darken-2; - border-bottom: tall $error-darken-2; - - padding: 0; - text-style: bold; - align-horizontal: center; -} - -Warning { - width: 100%; - height:3; - background: $warning; - color: $text-muted; - border-top: tall $warning-darken-2; - border-bottom: tall $warning-darken-2; - - text-style: bold; - align-horizontal: center; -} - -Success { - width: 100%; - - height:auto; - box-sizing: border-box; - background: $success; - color: $text-muted; - - border-top: hkey $success-darken-2; - border-bottom: hkey $success-darken-2; - - text-style: bold ; - - align-horizontal: center; -} - - -.horizontal { - layout: horizontal -} diff --git a/docs/examples/demo.py b/docs/examples/demo.py deleted file mode 100644 index fcea3e753..000000000 --- a/docs/examples/demo.py +++ /dev/null @@ -1,234 +0,0 @@ -from rich.console import RenderableType - -from rich.syntax import Syntax -from rich.text import Text - -from textual.app import App, ComposeResult -from textual.reactive import Reactive -from textual.widget import Widget -from textual.widgets import Static, DataTable, DirectoryTree, Header, Footer -from textual.layout import Container - -CODE = ''' -from __future__ import annotations - -from typing import Iterable, TypeVar - -T = TypeVar("T") - - -def loop_first(values: Iterable[T]) -> Iterable[tuple[bool, T]]: - """Iterate and generate a tuple with a flag for first value.""" - iter_values = iter(values) - try: - value = next(iter_values) - except StopIteration: - return - yield True, value - for value in iter_values: - yield False, value - - -def loop_last(values: Iterable[T]) -> Iterable[tuple[bool, T]]: - """Iterate and generate a tuple with a flag for last value.""" - iter_values = iter(values) - try: - previous_value = next(iter_values) - except StopIteration: - return - for value in iter_values: - yield False, previous_value - previous_value = value - yield True, previous_value - - -def loop_first_last(values: Iterable[T]) -> Iterable[tuple[bool, bool, T]]: - """Iterate and generate a tuple with a flag for first and last value.""" - iter_values = iter(values) - try: - previous_value = next(iter_values) - except StopIteration: - return - first = True - for value in iter_values: - yield first, False, previous_value - first = False - previous_value = value - yield first, True, previous_value -''' - - -lorem_short = """Lorem ipsum dolor sit amet, consectetur adipiscing elit. In velit liber a a a, volutpat nec hendrerit at, faucibus in odio. Aliquam hendrerit nibh sed quam volutpat maximus. Nullam suscipit convallis lorem quis sodales. In tristique lobortis ante et dictum. Ut at finibus ipsum.""" -lorem = ( - lorem_short - + """ In urna dolor, placerat et mi facilisis, congue sollicitudin massa. Phasellus felis turpis, cursus eu lectus et, porttitor malesuada augue. Sed feugiat volutpat velit, sollicitudin fringilla velit bibendum faucibus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. In velit libero, volutpat nec hendrerit at, faucibus in odio. Aliquam hendrerit nibh sed quam volutpat maximus. Nullam suscipit convallis lorem quis sodales. In tristique lobortis ante et dictum. Ut at finibus ipsum. In urna dolor, placerat et mi facilisis, congue sollicitudin massa. Phasellus felis turpis, cursus eu lectus et, porttitor malesuada augue. Sed feugiat volutpat velit, sollicitudin fringilla velit bibendum faucibus. """ -) - -lorem_short_text = Text.from_markup(lorem_short) -lorem_long_text = Text.from_markup(lorem * 2) - - -class TweetHeader(Widget): - def render(self) -> RenderableType: - return Text("Lorem Impsum", justify="center") - - -class TweetBody(Widget): - short_lorem = Reactive(False) - - def render(self) -> Text: - return lorem_short_text if self.short_lorem else lorem_long_text - - -class Tweet(Widget): - pass - - -class OptionItem(Widget): - def render(self) -> Text: - return Text("Option") - - -class Error(Widget): - def render(self) -> Text: - return Text("This is an error message", justify="center") - - -class Warning(Widget): - def render(self) -> Text: - return Text("This is a warning message", justify="center") - - -class Success(Widget): - def render(self) -> Text: - return Text("This is a success message", justify="center") - - -class BasicApp(App, css_path="basic.css"): - """A basic app demonstrating CSS""" - - def on_load(self): - """Bind keys here.""" - self.bind("s", "toggle_class('#sidebar', '-active')", description="Sidebar") - self.bind("d", "toggle_dark", description="Dark mode") - self.bind("q", "quit", description="Quit") - self.bind("f", "query_test", description="Query test") - - def compose(self): - yield Header() - - table = DataTable() - self.scroll_to_target = Tweet(TweetBody()) - - yield Container( - Tweet(TweetBody()), - Widget( - Static( - Syntax(CODE, "python", line_numbers=True, indent_guides=True), - classes="code", - ), - classes="scrollable", - ), - table, - Error(), - Tweet(TweetBody(), classes="scrollbar-size-custom"), - Warning(), - Tweet(TweetBody(), classes="scroll-horizontal"), - Success(), - Tweet(TweetBody(), classes="scroll-horizontal"), - Tweet(TweetBody(), classes="scroll-horizontal"), - Tweet(TweetBody(), classes="scroll-horizontal"), - Tweet(TweetBody(), classes="scroll-horizontal"), - Tweet(TweetBody(), classes="scroll-horizontal"), - ) - yield Widget( - Widget(classes="title"), - Widget(classes="user"), - OptionItem(), - OptionItem(), - OptionItem(), - Widget(classes="content"), - id="sidebar", - ) - yield Footer() - - 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(100): - table.add_row(*[f"Cell ([b]{n}[/b], {col})" for col in range(6)]) - - def on_mount(self): - self.sub_title = "Widget demo" - - async def on_key(self, event) -> None: - await self.dispatch_key(event) - - def action_toggle_dark(self): - self.dark = not self.dark - - def action_query_test(self): - query = self.query("Tweet") - self.log(query) - self.log(query.nodes) - self.log(query) - self.log(query.nodes) - - query.set_styles("outline: outer red;") - - query = query.exclude(".scroll-horizontal") - self.log(query) - self.log(query.nodes) - - # query = query.filter(".rubbish") - # self.log(query) - # self.log(query.first()) - - async def key_q(self): - await self.shutdown() - - def key_x(self): - self.panic(self.tree) - - def key_escape(self): - self.app.bell() - - def key_t(self): - # Pressing "t" toggles the content of the TweetBody widget, from a long "Lorem ipsum..." to a shorter one. - tweet_body = self.query("TweetBody").first() - tweet_body.short_lorem = not tweet_body.short_lorem - - def key_v(self): - self.get_child(id="content").scroll_to_widget(self.scroll_to_target) - - def key_space(self): - self.bell() - - -app = BasicApp() - -if __name__ == "__main__": - app.run() - - # from textual.geometry import Region - # from textual.color import Color - - # print(Region.intersection.cache_info()) - # print(Region.overlaps.cache_info()) - # print(Region.union.cache_info()) - # print(Region.split_vertical.cache_info()) - # print(Region.__contains__.cache_info()) - # from textual.css.scalar import Scalar - - # print(Scalar.resolve_dimension.cache_info()) - - # from rich.style import Style - # from rich.cells import cached_cell_len - - # print(Style._add.cache_info()) - - # print(cached_cell_len.cache_info()) diff --git a/docs/examples/events/custom01.py b/docs/examples/events/custom01.py index 30605af3d..043b32ffc 100644 --- a/docs/examples/events/custom01.py +++ b/docs/examples/events/custom01.py @@ -43,6 +43,6 @@ class ColorApp(App): self.screen.styles.animate("background", message.color, duration=0.5) -app = ColorApp() if __name__ == "__main__": + app = ColorApp() app.run() diff --git a/docs/examples/events/dictionary.py b/docs/examples/events/dictionary.py index b6810cc8e..ed95ceb0e 100644 --- a/docs/examples/events/dictionary.py +++ b/docs/examples/events/dictionary.py @@ -15,6 +15,8 @@ from textual.widgets import Static, TextInput class DictionaryApp(App): """Searches ab dictionary API as-you-type.""" + CSS_PATH = "dictionary.css" + def compose(self) -> ComposeResult: yield TextInput(placeholder="Search for a word") yield Vertical(Static(id="results", fluid=False), id="results-container") @@ -38,6 +40,6 @@ class DictionaryApp(App): self.query_one("#results", Static).update(JSON(results)) -app = DictionaryApp(css_path="dictionary.css") if __name__ == "__main__": + app = DictionaryApp() app.run() diff --git a/docs/examples/guide/dom1.py b/docs/examples/guide/dom1.py index e1f9fa14e..b45ebdb8c 100644 --- a/docs/examples/guide/dom1.py +++ b/docs/examples/guide/dom1.py @@ -5,6 +5,6 @@ class ExampleApp(App): pass -app = ExampleApp() if __name__ == "__main__": + app = ExampleApp() app.run() diff --git a/docs/examples/guide/dom2.py b/docs/examples/guide/dom2.py index b8b2913bf..35b670327 100644 --- a/docs/examples/guide/dom2.py +++ b/docs/examples/guide/dom2.py @@ -8,6 +8,6 @@ class ExampleApp(App): yield Footer() -app = ExampleApp() if __name__ == "__main__": + app = ExampleApp() app.run() diff --git a/docs/examples/guide/dom3.py b/docs/examples/guide/dom3.py index b90231fe3..e78ee5c05 100644 --- a/docs/examples/guide/dom3.py +++ b/docs/examples/guide/dom3.py @@ -20,6 +20,6 @@ class ExampleApp(App): ) -app = ExampleApp() if __name__ == "__main__": + app = ExampleApp() app.run() diff --git a/docs/examples/guide/dom4.css b/docs/examples/guide/dom4.css index 2f68d4160..d9169ee17 100644 --- a/docs/examples/guide/dom4.css +++ b/docs/examples/guide/dom4.css @@ -2,7 +2,7 @@ /* The top level dialog (a Container) */ #dialog { margin: 4 8; - background: $primary; + background: $panel; color: $text; border: tall $background; padding: 1 2; diff --git a/docs/examples/guide/dom4.py b/docs/examples/guide/dom4.py index 020133a26..dd12a4ea8 100644 --- a/docs/examples/guide/dom4.py +++ b/docs/examples/guide/dom4.py @@ -6,6 +6,8 @@ QUESTION = "Do you want to learn about Textual CSS?" class ExampleApp(App): + CSS_PATH = "dom4.css" + def compose(self) -> ComposeResult: yield Header() yield Footer() @@ -20,4 +22,6 @@ class ExampleApp(App): ) -app = ExampleApp(css_path="dom4.css") +if __name__ == "__main__": + app = ExampleApp() + app.run() diff --git a/docs/examples/guide/layout/center_layout.py b/docs/examples/guide/layout/center_layout.py index 973a256f0..0cf31c615 100644 --- a/docs/examples/guide/layout/center_layout.py +++ b/docs/examples/guide/layout/center_layout.py @@ -3,12 +3,14 @@ from textual.widgets import Static class CenterLayoutExample(App): + CSS_PATH = "center_layout.css" + def compose(self) -> ComposeResult: yield Static("One", id="bottom") yield Static("Two", id="middle") yield Static("Three", id="top") -app = CenterLayoutExample(css_path="center_layout.css") if __name__ == "__main__": + app = CenterLayoutExample() app.run() diff --git a/docs/examples/guide/layout/combining_layouts.py b/docs/examples/guide/layout/combining_layouts.py index 396b207ec..9dd34ab96 100644 --- a/docs/examples/guide/layout/combining_layouts.py +++ b/docs/examples/guide/layout/combining_layouts.py @@ -4,6 +4,8 @@ from textual.widgets import Static, Header class CombiningLayoutsExample(App): + CSS_PATH = "combining_layouts.css" + def compose(self) -> ComposeResult: yield Header() yield layout.Container( @@ -38,6 +40,6 @@ class CombiningLayoutsExample(App): print(self.stylesheet.variables["boost-lighten-2"]) -app = CombiningLayoutsExample(css_path="combining_layouts.css") if __name__ == "__main__": + app = CombiningLayoutsExample() app.run() diff --git a/docs/examples/guide/layout/dock_layout1_sidebar.py b/docs/examples/guide/layout/dock_layout1_sidebar.py index 9e0eced59..81eb94805 100644 --- a/docs/examples/guide/layout/dock_layout1_sidebar.py +++ b/docs/examples/guide/layout/dock_layout1_sidebar.py @@ -10,11 +10,13 @@ Docked widgets will not scroll out of view, making them ideal for sticky headers class DockLayoutExample(App): + CSS_PATH = "dock_layout1_sidebar.css" + def compose(self) -> ComposeResult: yield Static("Sidebar", id="sidebar") yield Static(TEXT * 10, id="body") -app = DockLayoutExample(css_path="dock_layout1_sidebar.css") if __name__ == "__main__": + app = DockLayoutExample() app.run() diff --git a/docs/examples/guide/layout/dock_layout2_sidebar.py b/docs/examples/guide/layout/dock_layout2_sidebar.py index cec0f931e..0da8f78c3 100644 --- a/docs/examples/guide/layout/dock_layout2_sidebar.py +++ b/docs/examples/guide/layout/dock_layout2_sidebar.py @@ -10,12 +10,14 @@ Docked widgets will not scroll out of view, making them ideal for sticky headers class DockLayoutExample(App): + CSS_PATH = "dock_layout2_sidebar.css" + def compose(self) -> ComposeResult: yield Static("Sidebar2", id="another-sidebar") yield Static("Sidebar1", id="sidebar") yield Static(TEXT * 10, id="body") -app = DockLayoutExample(css_path="dock_layout2_sidebar.css") +app = DockLayoutExample() if __name__ == "__main__": app.run() diff --git a/docs/examples/guide/layout/dock_layout3_sidebar_header.py b/docs/examples/guide/layout/dock_layout3_sidebar_header.py index eaa2a0b00..5967bda7f 100644 --- a/docs/examples/guide/layout/dock_layout3_sidebar_header.py +++ b/docs/examples/guide/layout/dock_layout3_sidebar_header.py @@ -10,12 +10,14 @@ Docked widgets will not scroll out of view, making them ideal for sticky headers class DockLayoutExample(App): + CSS_PATH = "dock_layout3_sidebar_header.css" + def compose(self) -> ComposeResult: yield Header(id="header") yield Static("Sidebar1", id="sidebar") yield Static(TEXT * 10, id="body") -app = DockLayoutExample(css_path="dock_layout3_sidebar_header.css") if __name__ == "__main__": + app = DockLayoutExample() app.run() diff --git a/docs/examples/guide/layout/grid_layout1.py b/docs/examples/guide/layout/grid_layout1.py index 35ea2e384..943f18cb7 100644 --- a/docs/examples/guide/layout/grid_layout1.py +++ b/docs/examples/guide/layout/grid_layout1.py @@ -3,6 +3,8 @@ from textual.widgets import Static class GridLayoutExample(App): + CSS_PATH = "grid_layout1.css" + def compose(self) -> ComposeResult: yield Static("One", classes="box") yield Static("Two", classes="box") @@ -12,6 +14,6 @@ class GridLayoutExample(App): yield Static("Six", classes="box") -app = GridLayoutExample(css_path="grid_layout1.css") if __name__ == "__main__": + app = GridLayoutExample() app.run() diff --git a/docs/examples/guide/layout/grid_layout2.py b/docs/examples/guide/layout/grid_layout2.py index 825d6e1c1..fa0094073 100644 --- a/docs/examples/guide/layout/grid_layout2.py +++ b/docs/examples/guide/layout/grid_layout2.py @@ -3,6 +3,8 @@ from textual.widgets import Static class GridLayoutExample(App): + CSS_PATH = "grid_layout1.css" + def compose(self) -> ComposeResult: yield Static("One", classes="box") yield Static("Two", classes="box") @@ -13,6 +15,6 @@ class GridLayoutExample(App): yield Static("Seven", classes="box") -app = GridLayoutExample(css_path="grid_layout1.css") if __name__ == "__main__": + app = GridLayoutExample() app.run() diff --git a/docs/examples/guide/layout/grid_layout3_row_col_adjust.py b/docs/examples/guide/layout/grid_layout3_row_col_adjust.py index e1a911005..c75a58da1 100644 --- a/docs/examples/guide/layout/grid_layout3_row_col_adjust.py +++ b/docs/examples/guide/layout/grid_layout3_row_col_adjust.py @@ -3,6 +3,8 @@ from textual.widgets import Static class GridLayoutExample(App): + CSS_PATH = "grid_layout3_row_col_adjust.css" + def compose(self) -> ComposeResult: yield Static("One", classes="box") yield Static("Two", classes="box") @@ -12,6 +14,6 @@ class GridLayoutExample(App): yield Static("Six", classes="box") -app = GridLayoutExample(css_path="grid_layout3_row_col_adjust.css") if __name__ == "__main__": + app = GridLayoutExample() app.run() diff --git a/docs/examples/guide/layout/grid_layout4_row_col_adjust.py b/docs/examples/guide/layout/grid_layout4_row_col_adjust.py index 62391b20f..f11a2d5b0 100644 --- a/docs/examples/guide/layout/grid_layout4_row_col_adjust.py +++ b/docs/examples/guide/layout/grid_layout4_row_col_adjust.py @@ -3,6 +3,8 @@ from textual.widgets import Static class GridLayoutExample(App): + CSS_PATH = "grid_layout4_row_col_adjust.css" + def compose(self) -> ComposeResult: yield Static("One", classes="box") yield Static("Two", classes="box") @@ -12,6 +14,6 @@ class GridLayoutExample(App): yield Static("Six", classes="box") -app = GridLayoutExample(css_path="grid_layout4_row_col_adjust.css") if __name__ == "__main__": + app = GridLayoutExample() app.run() diff --git a/docs/examples/guide/layout/grid_layout5_col_span.py b/docs/examples/guide/layout/grid_layout5_col_span.py index f9302621a..d7fe1cb83 100644 --- a/docs/examples/guide/layout/grid_layout5_col_span.py +++ b/docs/examples/guide/layout/grid_layout5_col_span.py @@ -3,6 +3,8 @@ from textual.widgets import Static class GridLayoutExample(App): + CSS_PATH = "grid_layout5_col_span.css" + def compose(self) -> ComposeResult: yield Static("One", classes="box") yield Static("Two [b](column-span: 2)", classes="box", id="two") @@ -12,6 +14,6 @@ class GridLayoutExample(App): yield Static("Six", classes="box") -app = GridLayoutExample(css_path="grid_layout5_col_span.css") if __name__ == "__main__": + app = GridLayoutExample() app.run() diff --git a/docs/examples/guide/layout/grid_layout6_row_span.py b/docs/examples/guide/layout/grid_layout6_row_span.py index 19cabbe5e..54630b081 100644 --- a/docs/examples/guide/layout/grid_layout6_row_span.py +++ b/docs/examples/guide/layout/grid_layout6_row_span.py @@ -3,6 +3,8 @@ from textual.widgets import Static class GridLayoutExample(App): + CSS_PATH = "grid_layout6_row_span.css" + def compose(self) -> ComposeResult: yield Static("One", classes="box") yield Static("Two [b](column-span: 2 and row-span: 2)", classes="box", id="two") @@ -12,6 +14,6 @@ class GridLayoutExample(App): yield Static("Six", classes="box") -app = GridLayoutExample(css_path="grid_layout6_row_span.css") +app = GridLayoutExample() if __name__ == "__main__": app.run() diff --git a/docs/examples/guide/layout/grid_layout7_gutter.py b/docs/examples/guide/layout/grid_layout7_gutter.py index 00f98e112..db916858c 100644 --- a/docs/examples/guide/layout/grid_layout7_gutter.py +++ b/docs/examples/guide/layout/grid_layout7_gutter.py @@ -3,6 +3,8 @@ from textual.widgets import Static class GridLayoutExample(App): + CSS_PATH = "grid_layout7_gutter.css" + def compose(self) -> ComposeResult: yield Static("One", classes="box") yield Static("Two", classes="box") @@ -12,6 +14,6 @@ class GridLayoutExample(App): yield Static("Six", classes="box") -app = GridLayoutExample(css_path="grid_layout7_gutter.css") if __name__ == "__main__": + app = GridLayoutExample() app.run() diff --git a/docs/examples/guide/layout/horizontal_layout.py b/docs/examples/guide/layout/horizontal_layout.py index cada52668..40997293f 100644 --- a/docs/examples/guide/layout/horizontal_layout.py +++ b/docs/examples/guide/layout/horizontal_layout.py @@ -3,12 +3,14 @@ from textual.widgets import Static class HorizontalLayoutExample(App): + CSS_PATH = "horizontal_layout.css" + def compose(self) -> ComposeResult: yield Static("One", classes="box") yield Static("Two", classes="box") yield Static("Three", classes="box") -app = HorizontalLayoutExample(css_path="horizontal_layout.css") if __name__ == "__main__": + app = HorizontalLayoutExample() app.run() diff --git a/docs/examples/guide/layout/horizontal_layout_overflow.py b/docs/examples/guide/layout/horizontal_layout_overflow.py index 8520c2b8b..b5be0e96d 100644 --- a/docs/examples/guide/layout/horizontal_layout_overflow.py +++ b/docs/examples/guide/layout/horizontal_layout_overflow.py @@ -3,12 +3,14 @@ from textual.widgets import Static class HorizontalLayoutExample(App): + CSS_PATH = "horizontal_layout_overflow.css" + def compose(self) -> ComposeResult: yield Static("One", classes="box") yield Static("Two", classes="box") yield Static("Three", classes="box") -app = HorizontalLayoutExample(css_path="horizontal_layout_overflow.css") if __name__ == "__main__": + app = HorizontalLayoutExample() app.run() diff --git a/docs/examples/guide/layout/layers.py b/docs/examples/guide/layout/layers.py index d26b5f18a..06afbd29a 100644 --- a/docs/examples/guide/layout/layers.py +++ b/docs/examples/guide/layout/layers.py @@ -3,11 +3,13 @@ from textual.widgets import Static class LayersExample(App): + CSS_PATH = "layers.css" + def compose(self) -> ComposeResult: yield Static("box1 (layer = above)", id="box1") yield Static("box2 (layer = below)", id="box2") -app = LayersExample(css_path="layers.css") if __name__ == "__main__": + app = LayersExample() app.run() diff --git a/docs/examples/guide/layout/offset.py b/docs/examples/guide/layout/offset.py index c55362ca4..738297307 100644 --- a/docs/examples/guide/layout/offset.py +++ b/docs/examples/guide/layout/offset.py @@ -12,6 +12,8 @@ class Box(Static): class OffsetExample(App): + CSS_PATH = "offset.css" + def compose(self) -> ComposeResult: yield layout.Container( Box(id="box1"), @@ -22,6 +24,6 @@ class OffsetExample(App): ) -app = OffsetExample(css_path="offset.css") if __name__ == "__main__": + app = OffsetExample() app.run() diff --git a/docs/examples/guide/layout/utility_containers.py b/docs/examples/guide/layout/utility_containers.py index 2ae40a2cc..fa44d0088 100644 --- a/docs/examples/guide/layout/utility_containers.py +++ b/docs/examples/guide/layout/utility_containers.py @@ -4,6 +4,8 @@ from textual.widgets import Static class UtilityContainersExample(App): + CSS_PATH = "utility_containers.css" + def compose(self) -> ComposeResult: yield layout.Horizontal( layout.Vertical( @@ -19,6 +21,6 @@ class UtilityContainersExample(App): ) -app = UtilityContainersExample(css_path="utility_containers.css") if __name__ == "__main__": + app = UtilityContainersExample() app.run() diff --git a/docs/examples/guide/layout/vertical_layout.py b/docs/examples/guide/layout/vertical_layout.py index 7c75dd592..233407ac3 100644 --- a/docs/examples/guide/layout/vertical_layout.py +++ b/docs/examples/guide/layout/vertical_layout.py @@ -3,12 +3,14 @@ from textual.widgets import Static class VerticalLayoutExample(App): + CSS_PATH = "vertical_layout.css" + def compose(self) -> ComposeResult: yield Static("One", classes="box") yield Static("Two", classes="box") yield Static("Three", classes="box") -app = VerticalLayoutExample(css_path="vertical_layout.css") if __name__ == "__main__": + app = VerticalLayoutExample() app.run() diff --git a/docs/examples/guide/layout/vertical_layout_scrolled.py b/docs/examples/guide/layout/vertical_layout_scrolled.py index 7adb2cc81..984040ef7 100644 --- a/docs/examples/guide/layout/vertical_layout_scrolled.py +++ b/docs/examples/guide/layout/vertical_layout_scrolled.py @@ -3,12 +3,14 @@ from textual.widgets import Static class VerticalLayoutScrolledExample(App): + CSS_PATH = "vertical_layout_scrolled.css" + def compose(self) -> ComposeResult: yield Static("One", classes="box") yield Static("Two", classes="box") yield Static("Three", classes="box") -app = VerticalLayoutScrolledExample(css_path="vertical_layout_scrolled.css") if __name__ == "__main__": + app = VerticalLayoutScrolledExample() app.run() diff --git a/docs/examples/guide/structure.py b/docs/examples/guide/structure.py index 11f285a74..fe1d3dea6 100644 --- a/docs/examples/guide/structure.py +++ b/docs/examples/guide/structure.py @@ -25,6 +25,6 @@ class ClockApp(App): yield Clock() -app = ClockApp() if __name__ == "__main__": + app = ClockApp() app.run() diff --git a/docs/examples/guide/styles/border01.py b/docs/examples/guide/styles/border01.py index 232478bc3..6cc5aada5 100644 --- a/docs/examples/guide/styles/border01.py +++ b/docs/examples/guide/styles/border01.py @@ -22,6 +22,6 @@ class BorderApp(App): self.widget.styles.border = ("heavy", "yellow") -app = BorderApp() if __name__ == "__main__": + app = BorderApp() app.run() diff --git a/docs/examples/guide/styles/box_sizing01.py b/docs/examples/guide/styles/box_sizing01.py index 6b1757220..af1e5ef62 100644 --- a/docs/examples/guide/styles/box_sizing01.py +++ b/docs/examples/guide/styles/box_sizing01.py @@ -33,6 +33,6 @@ class BoxSizing(App): self.widget2.styles.box_sizing = "content-box" -app = BoxSizing() if __name__ == "__main__": + app = BoxSizing() app.run() diff --git a/docs/examples/guide/styles/colors.py b/docs/examples/guide/styles/colors.py index b8115776c..5abfaf861 100644 --- a/docs/examples/guide/styles/colors.py +++ b/docs/examples/guide/styles/colors.py @@ -12,6 +12,6 @@ class WidgetApp(App): self.widget.styles.border = ("heavy", "white") -app = WidgetApp() if __name__ == "__main__": + app = WidgetApp() app.run() diff --git a/docs/examples/guide/styles/colors01.py b/docs/examples/guide/styles/colors01.py index 4c9af2b4a..401f9becb 100644 --- a/docs/examples/guide/styles/colors01.py +++ b/docs/examples/guide/styles/colors01.py @@ -19,6 +19,6 @@ class ColorApp(App): self.widget3.styles.background = Color(191, 78, 96) -app = ColorApp() if __name__ == "__main__": + app = ColorApp() app.run() diff --git a/docs/examples/guide/styles/colors02.py b/docs/examples/guide/styles/colors02.py index 51d7f005c..80b480e4e 100644 --- a/docs/examples/guide/styles/colors02.py +++ b/docs/examples/guide/styles/colors02.py @@ -15,6 +15,6 @@ class ColorApp(App): widget.styles.background = Color(191, 78, 96, a=alpha) -app = ColorApp() if __name__ == "__main__": + app = ColorApp() app.run() diff --git a/docs/examples/guide/styles/dimensions01.py b/docs/examples/guide/styles/dimensions01.py index 4f1292659..ed479f0f3 100644 --- a/docs/examples/guide/styles/dimensions01.py +++ b/docs/examples/guide/styles/dimensions01.py @@ -22,6 +22,6 @@ class DimensionsApp(App): self.widget.styles.height = 10 -app = DimensionsApp() if __name__ == "__main__": + app = DimensionsApp() app.run() diff --git a/docs/examples/guide/styles/dimensions02.py b/docs/examples/guide/styles/dimensions02.py index 769086fe2..339aade99 100644 --- a/docs/examples/guide/styles/dimensions02.py +++ b/docs/examples/guide/styles/dimensions02.py @@ -22,6 +22,6 @@ class DimensionsApp(App): self.widget.styles.height = "auto" -app = DimensionsApp() if __name__ == "__main__": + app = DimensionsApp() app.run() diff --git a/docs/examples/guide/styles/dimensions03.py b/docs/examples/guide/styles/dimensions03.py index f7a41293c..4d361227e 100644 --- a/docs/examples/guide/styles/dimensions03.py +++ b/docs/examples/guide/styles/dimensions03.py @@ -22,6 +22,6 @@ class DimensionsApp(App): self.widget.styles.height = "80%" -app = DimensionsApp() if __name__ == "__main__": + app = DimensionsApp() app.run() diff --git a/docs/examples/guide/styles/dimensions04.py b/docs/examples/guide/styles/dimensions04.py index 1cff8f651..405b5545e 100644 --- a/docs/examples/guide/styles/dimensions04.py +++ b/docs/examples/guide/styles/dimensions04.py @@ -25,6 +25,6 @@ class DimensionsApp(App): self.widget2.styles.height = "1fr" -app = DimensionsApp() if __name__ == "__main__": + app = DimensionsApp() app.run() diff --git a/docs/examples/guide/styles/margin01.py b/docs/examples/guide/styles/margin01.py index 1453d0ca5..7036cb725 100644 --- a/docs/examples/guide/styles/margin01.py +++ b/docs/examples/guide/styles/margin01.py @@ -27,6 +27,6 @@ class MarginApp(App): self.widget2.styles.margin = 2 -app = MarginApp() if __name__ == "__main__": + app = MarginApp() app.run() diff --git a/docs/examples/guide/styles/outline01.py b/docs/examples/guide/styles/outline01.py index dc120d227..cd77d0b8c 100644 --- a/docs/examples/guide/styles/outline01.py +++ b/docs/examples/guide/styles/outline01.py @@ -22,6 +22,6 @@ class OutlineApp(App): self.widget.styles.outline = ("heavy", "yellow") -app = OutlineApp() if __name__ == "__main__": + app = OutlineApp() app.run() diff --git a/docs/examples/guide/styles/padding01.py b/docs/examples/guide/styles/padding01.py index f87bdae73..92c68948a 100644 --- a/docs/examples/guide/styles/padding01.py +++ b/docs/examples/guide/styles/padding01.py @@ -22,6 +22,6 @@ class PaddingApp(App): self.widget.styles.padding = 2 -app = PaddingApp() if __name__ == "__main__": + app = PaddingApp() app.run() diff --git a/docs/examples/guide/styles/padding02.py b/docs/examples/guide/styles/padding02.py index 2900d78fe..50bf0b940 100644 --- a/docs/examples/guide/styles/padding02.py +++ b/docs/examples/guide/styles/padding02.py @@ -22,6 +22,6 @@ class PaddingApp(App): self.widget.styles.padding = (2, 4) -app = PaddingApp() if __name__ == "__main__": + app = PaddingApp() app.run() diff --git a/docs/examples/guide/styles/screen.py b/docs/examples/guide/styles/screen.py index 77caa300d..5a7b85fd7 100644 --- a/docs/examples/guide/styles/screen.py +++ b/docs/examples/guide/styles/screen.py @@ -7,6 +7,6 @@ class ScreenApp(App): self.screen.styles.border = ("heavy", "white") -app = ScreenApp() if __name__ == "__main__": + app = ScreenApp() app.run() diff --git a/docs/examples/guide/styles/widget.py b/docs/examples/guide/styles/widget.py index b8115776c..5abfaf861 100644 --- a/docs/examples/guide/styles/widget.py +++ b/docs/examples/guide/styles/widget.py @@ -12,6 +12,6 @@ class WidgetApp(App): self.widget.styles.border = ("heavy", "white") -app = WidgetApp() if __name__ == "__main__": + app = WidgetApp() app.run() diff --git a/docs/examples/simple.css b/docs/examples/simple.css deleted file mode 100644 index cf9a4a448..000000000 --- a/docs/examples/simple.css +++ /dev/null @@ -1,14 +0,0 @@ -Screen { - background: darkblue; - color: white; - layout: vertical; - align: center middle; -} -Static { - height: auto; - padding: 2; - margin: 2; - border: white; - background: #ffffff 30%; - content-align: center middle; -} diff --git a/docs/examples/simple.py b/docs/examples/simple.py deleted file mode 100644 index b4784ccd5..000000000 --- a/docs/examples/simple.py +++ /dev/null @@ -1,11 +0,0 @@ -from textual.app import App, ComposeResult -from textual.widgets import Static - - -class TextApp(App): - def compose(self) -> ComposeResult: - yield Static("Hello") - yield Static("[b]World![/b]") - - -app = TextApp(css_path="simple.css") diff --git a/docs/examples/tutorial/stopwatch.py b/docs/examples/tutorial/stopwatch.py index 2fae5fa7f..72e9e7139 100644 --- a/docs/examples/tutorial/stopwatch.py +++ b/docs/examples/tutorial/stopwatch.py @@ -71,6 +71,8 @@ class Stopwatch(Static): class StopwatchApp(App): """A Textual app to manage stopwatches.""" + CSS_PATH = "stopwatch.css" + BINDINGS = [ ("d", "toggle_dark", "Toggle dark mode"), ("a", "add_stopwatch", "Add"), @@ -100,6 +102,6 @@ class StopwatchApp(App): self.dark = not self.dark -app = StopwatchApp(css_path="stopwatch.css") if __name__ == "__main__": + app = StopwatchApp() app.run() diff --git a/docs/examples/tutorial/stopwatch01.py b/docs/examples/tutorial/stopwatch01.py index dd1940332..9f9a76043 100644 --- a/docs/examples/tutorial/stopwatch01.py +++ b/docs/examples/tutorial/stopwatch01.py @@ -17,6 +17,6 @@ class StopwatchApp(App): self.dark = not self.dark -app = StopwatchApp() if __name__ == "__main__": + app = StopwatchApp() app.run() diff --git a/docs/examples/tutorial/stopwatch02.py b/docs/examples/tutorial/stopwatch02.py index f76e4e8d1..ab1e843e1 100644 --- a/docs/examples/tutorial/stopwatch02.py +++ b/docs/examples/tutorial/stopwatch02.py @@ -34,6 +34,6 @@ class StopwatchApp(App): self.dark = not self.dark -app = StopwatchApp() if __name__ == "__main__": + app = StopwatchApp() app.run() diff --git a/docs/examples/tutorial/stopwatch03.py b/docs/examples/tutorial/stopwatch03.py index 5b390a087..7b879673a 100644 --- a/docs/examples/tutorial/stopwatch03.py +++ b/docs/examples/tutorial/stopwatch03.py @@ -21,6 +21,7 @@ class Stopwatch(Static): class StopwatchApp(App): """A Textual app to manage stopwatches.""" + CSS_PATH = "stopwatch03.css" BINDINGS = [("d", "toggle_dark", "Toggle dark mode")] def compose(self) -> ComposeResult: @@ -34,6 +35,6 @@ class StopwatchApp(App): self.dark = not self.dark -app = StopwatchApp(css_path="stopwatch03.css") if __name__ == "__main__": + app = StopwatchApp() app.run() diff --git a/docs/examples/tutorial/stopwatch04.py b/docs/examples/tutorial/stopwatch04.py index 536fd7d6a..8ad6e7cc3 100644 --- a/docs/examples/tutorial/stopwatch04.py +++ b/docs/examples/tutorial/stopwatch04.py @@ -28,6 +28,7 @@ class Stopwatch(Static): class StopwatchApp(App): """A Textual app to manage stopwatches.""" + CSS_PATH = "stopwatch04.css" BINDINGS = [("d", "toggle_dark", "Toggle dark mode")] def compose(self) -> ComposeResult: @@ -41,6 +42,6 @@ class StopwatchApp(App): self.dark = not self.dark -app = StopwatchApp(css_path="stopwatch04.css") if __name__ == "__main__": + app = StopwatchApp() app.run() diff --git a/docs/examples/tutorial/stopwatch05.py b/docs/examples/tutorial/stopwatch05.py index 3bdba5cd8..b2281515b 100644 --- a/docs/examples/tutorial/stopwatch05.py +++ b/docs/examples/tutorial/stopwatch05.py @@ -48,6 +48,7 @@ class Stopwatch(Static): class StopwatchApp(App): """A Textual app to manage stopwatches.""" + CSS_PATH = "stopwatch04.css" BINDINGS = [("d", "toggle_dark", "Toggle dark mode")] def compose(self) -> ComposeResult: @@ -61,6 +62,6 @@ class StopwatchApp(App): self.dark = not self.dark -app = StopwatchApp(css_path="stopwatch04.css") if __name__ == "__main__": + app = StopwatchApp() app.run() diff --git a/docs/examples/tutorial/stopwatch06.py b/docs/examples/tutorial/stopwatch06.py index 260a287c2..04a609322 100644 --- a/docs/examples/tutorial/stopwatch06.py +++ b/docs/examples/tutorial/stopwatch06.py @@ -71,6 +71,7 @@ class Stopwatch(Static): class StopwatchApp(App): """A Textual app to manage stopwatches.""" + CSS_PATH = "stopwatch04.css" BINDINGS = [("d", "toggle_dark", "Toggle dark mode")] def compose(self) -> ComposeResult: @@ -84,6 +85,6 @@ class StopwatchApp(App): self.dark = not self.dark -app = StopwatchApp(css_path="stopwatch04.css") if __name__ == "__main__": + app = StopwatchApp() app.run() diff --git a/docs/examples/widgets/button.py b/docs/examples/widgets/button.py index f32602579..74a910767 100644 --- a/docs/examples/widgets/button.py +++ b/docs/examples/widgets/button.py @@ -4,6 +4,8 @@ from textual.widgets import Button, Static class ButtonsApp(App): + CSS_PATH = "button.css" + def compose(self) -> ComposeResult: yield layout.Horizontal( layout.Vertical( @@ -28,7 +30,6 @@ class ButtonsApp(App): self.app.bell() -app = ButtonsApp(css_path="button.css") - if __name__ == "__main__": + app = ButtonsApp() result = app.run() diff --git a/docs/guide/CSS.md b/docs/guide/CSS.md index 610bb5176..ded358811 100644 --- a/docs/guide/CSS.md +++ b/docs/guide/CSS.md @@ -111,7 +111,7 @@ Both Header and Footer are children of the Screen object. To further explore the DOM, we're going to build a simple dialog with a question and two buttons. To do this we're going import and use a few more builtin widgets: -- `texual.layout.Container` For our top-level dialog. +- `textual.layout.Container` For our top-level dialog. - `textual.layout.Horizontal` To arrange widgets left to right. - `textual.widgets.Static` For simple content. - `textual.widgets.Button` For a clickable button. @@ -140,13 +140,13 @@ You may recognize some of the elements in the above screenshot, but it doesn't q ## CSS files -To add a stylesheet we pass the path to the app with the `css_path` parameter: +To add a stylesheet set the `CSS_PATH` classvar to a relative path: -```python hl_lines="23" +```python hl_lines="9" --8<-- "docs/examples/guide/dom4.py" ``` -You may have noticed that some of the constructors have additional keyword argument: `id` and `classes`. These are used by the CSS to identify parts of the DOM. We will cover these in the next section. +You may have noticed that some of the constructors have additional keyword arguments: `id` and `classes`. These are used by the CSS to identify parts of the DOM. We will cover these in the next section. Here's the CSS file we are applying: diff --git a/docs/guide/app.md b/docs/guide/app.md index f5e6defe3..ff64bbe9f 100644 --- a/docs/guide/app.md +++ b/docs/guide/app.md @@ -10,6 +10,7 @@ The first step in building a Textual app is to import the [App][textual.app.App] --8<-- "docs/examples/app/simple01.py" ``` + ### The run method To run an app we create an instance and call [run()][textual.app.App.run]. @@ -153,9 +154,9 @@ Textual apps can reference [CSS](CSS.md) files which define how your app and wid The chapter on [Textual CSS](CSS.md) describes how to use CSS in detail. For now lets look at how your app references external CSS files. -The following example sets the `css_path` attribute on the app: +The following example enables loading of CSS by adding a `CSS_PATH` class variable: -```python title="question02.py" hl_lines="15" +```python title="question02.py" hl_lines="6" --8<-- "docs/examples/app/question02.py" ``` @@ -172,7 +173,9 @@ When `"question02.py"` runs it will load `"question02.css"` and update the app a ### Classvar CSS -While external CSS files are recommended for most applications, and enable some cool features like *live editing* (see below), you can also specify the CSS directly within the Python code. To do this you can set the `CSS` class variable on the app which contains the CSS content. +While external CSS files are recommended for most applications, and enable some cool features like *live editing*, you can also specify the CSS directly within the Python code. + +To do this set a `CSS` class variable on the app to a string containing your CSS. Here's the question app with classvar CSS: diff --git a/docs/guide/devtools.md b/docs/guide/devtools.md index 140554268..e6bb72ac6 100644 --- a/docs/guide/devtools.md +++ b/docs/guide/devtools.md @@ -118,9 +118,8 @@ class LogApp(App): def on_mount(self): self.log(self.tree) -app = LogApp() if __name__ == "__main__": - app.run() + LogApp.run() ``` diff --git a/docs/guide/events.md b/docs/guide/events.md index 60127e012..762f924e8 100644 --- a/docs/guide/events.md +++ b/docs/guide/events.md @@ -172,7 +172,7 @@ Let's look at an example which looks up word definitions from an [api](https://d === "dictionary.py" - ```python title="dictionary.py" hl_lines="26" + ```python title="dictionary.py" hl_lines="28" --8<-- "docs/examples/events/dictionary.py" ``` === "dictionary.css" diff --git a/docs/tutorial.md b/docs/tutorial.md index 6a62b427e..413826c2f 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -200,11 +200,11 @@ While it's possible to set all styles for an app this way, it is rarely necessar Let's add a CSS file to our application. -```python title="stopwatch03.py" hl_lines="37" +```python title="stopwatch03.py" hl_lines="24" --8<-- "docs/examples/tutorial/stopwatch03.py" ``` -Adding the `css_path` attribute to the app constructor tells Textual to load the following file when it starts the app: +Adding the `CSS_PATH` class variable tells Textual to load the following file when it starts the app: ```sass title="stopwatch03.css" --8<-- "docs/examples/tutorial/stopwatch03.css" @@ -423,7 +423,7 @@ To add a new child widget call `mount()` on the parent. To remove a widget, call Let's use these to implement adding and removing stopwatches to our app. -```python title="stopwatch.py" hl_lines="76-77 86-90 92-96" +```python title="stopwatch.py" hl_lines="78-79 88-92 94-98" --8<-- "docs/examples/tutorial/stopwatch.py" ``` diff --git a/e2e_tests/test_apps/basic.py b/e2e_tests/test_apps/basic.py index a81cdbf46..3391e68e5 100644 --- a/e2e_tests/test_apps/basic.py +++ b/e2e_tests/test_apps/basic.py @@ -104,9 +104,11 @@ class Success(Widget): return Text("This is a success message", justify="center") -class BasicApp(App, css_path="basic.css"): +class BasicApp(App): """A basic app demonstrating CSS""" + CSS_PATH = "basic.css" + def on_load(self): """Bind keys here.""" self.bind("s", "toggle_class('#sidebar', '-active')", description="Sidebar") @@ -210,9 +212,8 @@ class BasicApp(App, css_path="basic.css"): self.bell() -app = BasicApp() - if __name__ == "__main__": + app = BasicApp() app.run(quit_after=2) # from textual.geometry import Region diff --git a/examples/borders.py b/examples/borders.py deleted file mode 100644 index b3f8ece6d..000000000 --- a/examples/borders.py +++ /dev/null @@ -1,29 +0,0 @@ -from itertools import cycle - -from textual.app import App -from textual.color import Color -from textual.constants import BORDERS -from textual.widgets import Static - - -class BorderApp(App): - """Displays a pride flag.""" - - COLORS = ["red", "orange", "yellow", "green", "blue", "purple"] - - def compose(self): - self.dark = True - for border, color in zip(BORDERS, cycle(self.COLORS)): - static = Static(f"border: {border} {color};") - static.styles.height = 7 - static.styles.background = Color.parse(color).with_alpha(0.2) - static.styles.margin = (1, 2) - static.styles.border = (border, color) - static.styles.content_align = ("center", "middle") - yield static - - -app = BorderApp() - -if __name__ == "__main__": - app.run() diff --git a/examples/calculator.py b/examples/calculator.py index 2174f7f9b..a1f1061f9 100644 --- a/examples/calculator.py +++ b/examples/calculator.py @@ -10,6 +10,8 @@ from textual.widgets import Button, Static class CalculatorApp(App): """A working 'desktop' calculator.""" + CSS_PATH = "calculator.css" + numbers = var("0") show_ac = var(True) left = var(Decimal("0")) @@ -137,6 +139,5 @@ class CalculatorApp(App): do_math() -app = CalculatorApp(css_path="calculator.css") if __name__ == "__main__": - app.run() + CalculatorApp().run() diff --git a/examples/code_browser.py b/examples/code_browser.py index 1b434d487..4d1c4ee97 100644 --- a/examples/code_browser.py +++ b/examples/code_browser.py @@ -12,6 +12,7 @@ from textual.widgets import DirectoryTree, Footer, Header, Static class CodeBrowser(App): """Textual code browser app.""" + CSS_PATH = "code_browser.css" BINDINGS = [ ("f", "toggle_files", "Toggle Files"), ("q", "quit", "Quit"), @@ -56,6 +57,5 @@ class CodeBrowser(App): self.show_tree = not self.show_tree -app = CodeBrowser(css_path="code_browser.css") if __name__ == "__main__": - app.run() + CodeBrowser().run() diff --git a/examples/pride.py b/examples/pride.py index d6da93797..5253e0851 100644 --- a/examples/pride.py +++ b/examples/pride.py @@ -15,7 +15,5 @@ class PrideApp(App): yield stripe -app = PrideApp() - if __name__ == "__main__": - app.run() + PrideApp().run() diff --git a/sandbox/align.py b/sandbox/align.py index c9a73a733..f3c3dc0f0 100644 --- a/sandbox/align.py +++ b/sandbox/align.py @@ -11,6 +11,8 @@ class Thing(Widget): class AlignApp(App): + CSS_PATH = "align.css" + def on_load(self): self.bind("t", "log_tree") @@ -23,7 +25,6 @@ class AlignApp(App): self.log(self.screen.tree) -app = AlignApp(css_path="align.css") - if __name__ == "__main__": + app = AlignApp(css_path="align.css") app.run() diff --git a/sandbox/auto_test.py b/sandbox/auto_test.py index 594985a89..c3a071801 100644 --- a/sandbox/auto_test.py +++ b/sandbox/auto_test.py @@ -8,13 +8,14 @@ TEXT = Text.from_markup(" ".join(str(n) * 5 for n in range(12))) class AutoApp(App): + CSS_PATH = "auto_test.css" + def compose(self) -> ComposeResult: yield Vertical( Static(TEXT, classes="test"), Static(TEXT, id="test", classes="test") ) -app = AutoApp(css_path="auto_test.css") - if __name__ == "__main__": + app = AutoApp() app.run() diff --git a/sandbox/borders.py b/sandbox/borders.py index e67456ffa..7dcb2dacd 100644 --- a/sandbox/borders.py +++ b/sandbox/borders.py @@ -61,7 +61,6 @@ class MyTestApp(App): self.bind("q", "quit") -app = MyTestApp() - if __name__ == "__main__": + app = MyTestApp() app.run() diff --git a/src/textual/_doc.py b/src/textual/_doc.py index 9bd3b99ca..e4974924b 100644 --- a/src/textual/_doc.py +++ b/src/textual/_doc.py @@ -1,9 +1,11 @@ from __future__ import annotations -import runpy import os +import runpy import shlex -from typing import cast, TYPE_CHECKING +from typing import TYPE_CHECKING, cast + +from textual._import_app import AppFail, import_app if TYPE_CHECKING: from textual.app import App @@ -28,10 +30,7 @@ def format_svg(source, language, css_class, options, md, attrs, **kwargs) -> str cwd = os.getcwd() try: - app_vars = runpy.run_path(path) - if "sys" in app_vars: - app_vars["sys"].argv = cmd - app: App = cast("App", app_vars["app"]) + app = import_app(path) app.run( quit_after=5, press=press or ["ctrl+c"], @@ -55,9 +54,10 @@ def format_svg(source, language, css_class, options, md, attrs, **kwargs) -> str def rich(source, language, css_class, options, md, attrs, **kwargs) -> str: """A superfences formatter to insert a SVG screenshot.""" - from rich.console import Console import io + from rich.console import Console + title = attrs.get("title", "Rich") console = Console( diff --git a/src/textual/_import_app.py b/src/textual/_import_app.py new file mode 100644 index 000000000..154fc7198 --- /dev/null +++ b/src/textual/_import_app.py @@ -0,0 +1,101 @@ +from __future__ import annotations + +import os +import runpy +import shlex +from typing import cast, TYPE_CHECKING + + +if TYPE_CHECKING: + from textual.app import App + + +class AppFail(Exception): + pass + + +def import_app(import_name: str) -> App: + """Import an app from a path or import name. + + Args: + import_name (str): A name to import, such as `foo.bar`, or a path ending with .py. + + Raises: + AppFail: If the app could not be found for any reason. + + Returns: + App: A Textual application + """ + + import inspect + import importlib + import sys + + from textual.app import App + + import_name, *argv = shlex.split(import_name) + lib, _colon, name = import_name.partition(":") + + if lib.endswith(".py"): + path = os.path.abspath(lib) + try: + global_vars = runpy.run_path(path, {}) + except Exception as error: + raise AppFail(str(error)) + + if "sys" in global_vars: + global_vars["sys"].argv = [path, *argv] + + if name: + # User has given a name, use that + try: + app = global_vars[name] + except KeyError: + raise AppFail(f"App {name!r} not found in {lib!r}") + else: + # User has not given a name + if "app" in global_vars: + # App exists, lets use that + try: + app = global_vars["app"] + except KeyError: + raise AppFail(f"App {name!r} not found in {lib!r}") + else: + # Find a App class or instance that is *not* the base class + apps = [ + value + for value in global_vars.values() + if ( + isinstance(value, App) + or (inspect.isclass(value) and issubclass(value, App)) + and value is not App + ) + ] + if not apps: + raise AppFail( + f'Unable to find app in {lib!r}, try specifying app with "foo.py:app"' + ) + if len(apps) > 1: + raise AppFail( + f'Multiple apps found {lib!r}, try specifying app with "foo.py:app"' + ) + app = apps[0] + app._BASE_PATH = path + + else: + # Assuming the user wants to import the file + sys.path.append("") + try: + module = importlib.import_module(lib) + except ImportError as error: + raise AppFail(str(error)) + + try: + app = getattr(module, name or "app") + except AttributeError: + raise AppFail(f"Unable to find {name!r} in {module!r}") + + if inspect.isclass(app) and issubclass(app, App): + app = app() + + return cast(App, app) diff --git a/src/textual/_path.py b/src/textual/_path.py index 3a0f5f4bc..28a9ba1bc 100644 --- a/src/textual/_path.py +++ b/src/textual/_path.py @@ -24,7 +24,10 @@ def _make_path_object_relative(path: str | PurePath, obj: object) -> Path: return path # Otherwise (relative path), resolve it relative to obj... - subclass_module = sys.modules[obj.__module__] - subclass_path = Path(inspect.getfile(subclass_module)) + base_path = getattr(obj, "_BASE_PATH", None) + if base_path is not None: + subclass_path = Path(base_path) + else: + subclass_path = Path(inspect.getfile(obj.__class__)) resolved_path = (subclass_path.parent / path).resolve() return resolved_path diff --git a/src/textual/app.py b/src/textual/app.py index 0c5956d6e..68926a074 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -152,6 +152,7 @@ class App(Generic[ReturnType], DOMNode): SCREENS: dict[str, Screen] = {} + _BASE_PATH: str | None = None CSS_PATH: str | None = None focused: Reactive[Widget | None] = Reactive(None) @@ -232,12 +233,6 @@ class App(Generic[ReturnType], DOMNode): ) self._screenshot: str | None = None - def __init_subclass__( - cls, css_path: str | None = None, inherit_css: bool = True - ) -> None: - super().__init_subclass__(inherit_css=inherit_css) - cls.CSS_PATH = css_path - title: Reactive[str] = Reactive("Textual") sub_title: Reactive[str] = Reactive("") dark: Reactive[bool] = Reactive(True) diff --git a/src/textual/cli/cli.py b/src/textual/cli/cli.py index d83275b47..7b856b689 100644 --- a/src/textual/cli/cli.py +++ b/src/textual/cli/cli.py @@ -1,15 +1,11 @@ from __future__ import annotations -import os -import runpy -import shlex -from typing import cast, TYPE_CHECKING - -from importlib_metadata import version +from typing import TYPE_CHECKING import click - +from importlib_metadata import version from textual.devtools.server import _run_devtools +from textual._import_app import import_app, AppFail if TYPE_CHECKING: from textual.app import App @@ -36,96 +32,6 @@ def console(verbose: bool, exclude: list[str]) -> None: console.show_cursor(True) -class AppFail(Exception): - pass - - -def import_app(import_name: str) -> App: - """Import an app from it's import name. - - Args: - import_name (str): A name to import, such as `foo.bar` - - Raises: - AppFail: If the app could not be found for any reason. - - Returns: - App: A Textual application - """ - - import inspect - import importlib - import sys - - from textual.app import App - - import_name, *argv = shlex.split(import_name) - lib, _colon, name = import_name.partition(":") - - if lib.endswith(".py"): - path = os.path.abspath(lib) - try: - global_vars = runpy.run_path(path, {}) - except Exception as error: - raise AppFail(str(error)) - - if "sys" in global_vars: - global_vars["sys"].argv = [path, *argv] - - if name: - # User has given a name, use that - try: - app = global_vars[name] - except KeyError: - raise AppFail(f"App {name!r} not found in {lib!r}") - else: - # User has not given a name - if "app" in global_vars: - # App exists, lets use that - try: - app = global_vars["app"] - except KeyError: - raise AppFail(f"App {name!r} not found in {lib!r}") - else: - # Find a App class or instance that is *not* the base class - apps = [ - value - for value in global_vars.values() - if ( - isinstance(value, App) - or (inspect.isclass(value) and issubclass(value, App)) - and value is not App - ) - ] - if not apps: - raise AppFail( - f'Unable to find app in {lib!r}, try specifying app with "foo.py:app"' - ) - if len(apps) > 1: - raise AppFail( - f'Multiple apps found {lib!r}, try specifying app with "foo.py:app"' - ) - app = apps[0] - - else: - # Assuming the user wants to import the file - sys.path.append("") - try: - module = importlib.import_module(lib) - except ImportError as error: - raise AppFail(str(error)) - - try: - app = getattr(module, name or "app") - except AttributeError: - raise AppFail(f"Unable to find {name!r} in {module!r}") - - if inspect.isclass(app) and issubclass(app, App): - app = app() - - return cast(App, app) - - @run.command( "run", context_settings={