From e987974d6732e71e378fbe4c79e422efde963fc0 Mon Sep 17 00:00:00 2001 From: Leodanis Pozo Ramos Date: Wed, 19 Feb 2025 11:08:56 +0100 Subject: [PATCH 1/2] Sample code for the article on Textual --- python-textual/README.md | 3 ++ python-textual/buttons_and_inputs.py | 23 +++++++++++ python-textual/events.py | 47 +++++++++++++++++++++++ python-textual/events.tcss | 16 ++++++++ python-textual/grid.py | 21 ++++++++++ python-textual/grid.tcss | 9 +++++ python-textual/grid_with_tcss.py | 16 ++++++++ python-textual/hello_textual.py | 10 +++++ python-textual/horizontal_layout.py | 18 +++++++++ python-textual/horizontal_scroll.py | 18 +++++++++ python-textual/layouts.py.py | 36 +++++++++++++++++ python-textual/layouts.tcss | 21 ++++++++++ python-textual/static_and_label.py | 32 +++++++++++++++ python-textual/static_and_label.tcss | 23 +++++++++++ python-textual/static_and_label_tcss.py | 24 ++++++++++++ python-textual/vertical_layout.py.py | 22 +++++++++++ python-textual/vertical_layout.tcss | 3 ++ python-textual/vertical_layout_tcss.py.py | 17 ++++++++ python-textual/vertical_scroll.py | 17 ++++++++ 19 files changed, 376 insertions(+) create mode 100644 python-textual/README.md create mode 100644 python-textual/buttons_and_inputs.py create mode 100644 python-textual/events.py create mode 100644 python-textual/events.tcss create mode 100644 python-textual/grid.py create mode 100644 python-textual/grid.tcss create mode 100644 python-textual/grid_with_tcss.py create mode 100644 python-textual/hello_textual.py create mode 100644 python-textual/horizontal_layout.py create mode 100644 python-textual/horizontal_scroll.py create mode 100644 python-textual/layouts.py.py create mode 100644 python-textual/layouts.tcss create mode 100644 python-textual/static_and_label.py create mode 100644 python-textual/static_and_label.tcss create mode 100644 python-textual/static_and_label_tcss.py create mode 100644 python-textual/vertical_layout.py.py create mode 100644 python-textual/vertical_layout.tcss create mode 100644 python-textual/vertical_layout_tcss.py.py create mode 100644 python-textual/vertical_scroll.py diff --git a/python-textual/README.md b/python-textual/README.md new file mode 100644 index 0000000000..f9629fae3a --- /dev/null +++ b/python-textual/README.md @@ -0,0 +1,3 @@ +# Python Textual: Build Beautiful UIs in the Terminal + +This folder provides the code examples for the Real Python tutorial [Python Textual: Build Beautiful UIs in the Terminal](https://realpython.com/python-textual/). diff --git a/python-textual/buttons_and_inputs.py b/python-textual/buttons_and_inputs.py new file mode 100644 index 0000000000..12025fedb8 --- /dev/null +++ b/python-textual/buttons_and_inputs.py @@ -0,0 +1,23 @@ +from textual.app import App +from textual.widgets import Button, Input + +class ButtonsAndInputsApp(App): + def compose(self): + # Buttons + yield Button("Click me!") + yield Button("Primary!", variant="primary") + yield Button.success("Success!") + yield Button.warning("Warning!") + yield Button.error("Error!") + # Inputs + yield Input(placeholder="Type your text here") + yield Input(placeholder="Password", password=True) + yield Input( + placeholder="Type a number here", + type="number", + tooltip="Digits only please!", + ) + +if __name__ == "__main__": + app = ButtonsAndInputsApp() + app.run() diff --git a/python-textual/events.py b/python-textual/events.py new file mode 100644 index 0000000000..2d6393f6a3 --- /dev/null +++ b/python-textual/events.py @@ -0,0 +1,47 @@ +# from textual import on +from textual.app import App +from textual.widgets import Button, Digits, Footer + +class EventsApp(App): + CSS_PATH = "events.tcss" + BINDINGS = [ + ("q", "quit", "Quit"), + ("b", "toggle_border", "Toggle border"), + ] + + presses_count = 0 + double_border = False + + def compose(self): + yield Button( + "Click me!", + id="button", + ) + digits = Digits("0", id="digits") + digits.border_subtitle = "Button presses" + yield digits + yield Footer() + + def action_toggle_border(self): + self.double_border = not self.double_border + digits = self.query_one("#digits") + if self.double_border: + digits.styles.border = ("double", "yellow") + else: + digits.styles.border = ("solid", "white") + + def on_button_pressed(self, event): + if event.button.id == "button": + self.presses_count += 1 + digits = self.query_one("#digits") + digits.update(f"{self.presses_count}") + + # @on(Button.Pressed, "#button") + # def button_pressed(self, event): + # self.presses_count += 1 + # digits = self.query_one("#digits") + # digits.update(f"{self.presses_count}") + +if __name__ == "__main__": + app = EventsApp() + app.run() diff --git a/python-textual/events.tcss b/python-textual/events.tcss new file mode 100644 index 0000000000..d450c0c51a --- /dev/null +++ b/python-textual/events.tcss @@ -0,0 +1,16 @@ +Button { + background: $secondary; + border: solid $primary; + margin: 2 2; +} + +Button:hover { + border: round white; +} + +#digits { + color: green; + border: solid white; + padding: 1; + width: 30; +} \ No newline at end of file diff --git a/python-textual/grid.py b/python-textual/grid.py new file mode 100644 index 0000000000..cd2836c165 --- /dev/null +++ b/python-textual/grid.py @@ -0,0 +1,21 @@ +from textual.app import App +from textual.containers import Grid +from textual.widgets import Static + +class GridLayoutExample(App): + def compose(self): + grid = Grid() + grid.styles.grid_size_rows = rows = 6 + grid.styles.grid_size_columns = cols = 4 + with grid: + for row in range(rows): + for col in range(cols): + static = Static(f"Static ({row=}, {col=})") + static.styles.border = ("solid", "green") + static.styles.width = "1fr" + static.styles.height = "1fr" + yield static + +if __name__ == "__main__": + app = GridLayoutExample() + app.run() diff --git a/python-textual/grid.tcss b/python-textual/grid.tcss new file mode 100644 index 0000000000..443ffc6ee2 --- /dev/null +++ b/python-textual/grid.tcss @@ -0,0 +1,9 @@ +Grid { + grid_size: 4 6; +} + +Static { + height: 1fr; + width: 1fr; + border: solid green; +} \ No newline at end of file diff --git a/python-textual/grid_with_tcss.py b/python-textual/grid_with_tcss.py new file mode 100644 index 0000000000..5a5f1053ef --- /dev/null +++ b/python-textual/grid_with_tcss.py @@ -0,0 +1,16 @@ +from textual.app import App +from textual.containers import Grid +from textual.widgets import Static + +class GridLayoutWithTCSS(App): + CSS_PATH = "grid.tcss" + + def compose(self): + with Grid(): + for row in range(6): + for col in range(4): + yield Static(f"Static ({row=}, {col=})") + +if __name__ == "__main__": + app = GridLayoutWithTCSS() + app.run() diff --git a/python-textual/hello_textual.py b/python-textual/hello_textual.py new file mode 100644 index 0000000000..eaad495b1a --- /dev/null +++ b/python-textual/hello_textual.py @@ -0,0 +1,10 @@ +from textual.app import App +from textual.widgets import Static + +class HelloTextualApp(App): + def compose(self): + yield Static("Hello, Textual!") + +if __name__ == "__main__": + app = HelloTextualApp() + app.run() diff --git a/python-textual/horizontal_layout.py b/python-textual/horizontal_layout.py new file mode 100644 index 0000000000..029066d2e0 --- /dev/null +++ b/python-textual/horizontal_layout.py @@ -0,0 +1,18 @@ +from textual.app import App +from textual.containers import Horizontal +from textual.widgets import Static + +NUM_BOXES = 4 + +class HorizontalLayoutExample(App): + def compose(self): + with Horizontal(): + for i in range(NUM_BOXES): + static = Static(f"Static {i+1}") + static.styles.border = ("solid", "green") + static.styles.width="10%" + yield static + +if __name__ == "__main__": + app = HorizontalLayoutExample() + app.run() diff --git a/python-textual/horizontal_scroll.py b/python-textual/horizontal_scroll.py new file mode 100644 index 0000000000..9a11f1b522 --- /dev/null +++ b/python-textual/horizontal_scroll.py @@ -0,0 +1,18 @@ +from textual.app import App +from textual.containers import HorizontalScroll +from textual.widgets import Static + +NUM_BOXES = 20 + +class HorizontalScrollExample(App): + def compose(self): + with HorizontalScroll(): + for i in range(NUM_BOXES): + static = Static(f"Static {i+1}") + static.styles.border = ("solid", "green") + static.styles.width="10%" + yield static + +if __name__ == "__main__": + app = HorizontalScrollExample() + app.run() diff --git a/python-textual/layouts.py.py b/python-textual/layouts.py.py new file mode 100644 index 0000000000..12d2315013 --- /dev/null +++ b/python-textual/layouts.py.py @@ -0,0 +1,36 @@ +from textual.app import App +from textual.containers import ( + Horizontal, + HorizontalScroll, + VerticalScroll, +) +from textual.widgets import Label, Static + +NUM_BOXES = 12 + +class NestedContainersExample(App): + CSS_PATH = "layouts.tcss" + + def compose(self): + with Horizontal(id="horizontal"): + yield Static("Left", classes="box") + with HorizontalScroll(id="horizontalscroll"): + for i in range(NUM_BOXES): + yield Static( + f"Center.{i+1}", + classes="box yellowbox", + ) + with VerticalScroll(id="verticalscroll"): + for i in range(NUM_BOXES): + yield Static( + f"Right.{i+1}", + classes="box redbox", + ) + yield Label( + "I am a docked label.\nI don't move!", + id="docked-label", + ) + +if __name__ == "__main__": + app = NestedContainersExample() + app.run() diff --git a/python-textual/layouts.tcss b/python-textual/layouts.tcss new file mode 100644 index 0000000000..7a8b793c77 --- /dev/null +++ b/python-textual/layouts.tcss @@ -0,0 +1,21 @@ +.box { + height: 1fr; + width: 1fr; + background:$panel; + border: solid white; +} + +.redbox { + border: heavy red; + height: 5; +} + +.yellowbox { + border: heavy yellow; + width: 10; +} + +#docked-label { + dock: bottom; + border: solid dodgerblue; +} \ No newline at end of file diff --git a/python-textual/static_and_label.py b/python-textual/static_and_label.py new file mode 100644 index 0000000000..5158a367da --- /dev/null +++ b/python-textual/static_and_label.py @@ -0,0 +1,32 @@ +from textual.app import App +from textual.widgets import Label, Static + + +class StaticAndLabelApp(App): + def compose(self): + self.static = Static( + "I am a [bold red]Static[/bold red] widget!", + ) + yield self.static + self.label = Label( + "I am a [yellow italic]Label[/yellow italic] widget!", + ) + yield self.label + + def on_mount(self): + # Styling the static + self.static.styles.background = "blue" + self.static.styles.border = ("solid", "white") + self.static.styles.text_align = "center" + self.static.styles.padding = 1, 1 + self.static.styles.margin = 4, 4 + # Styling the label + self.label.styles.background = "darkgreen" + self.label.styles.border = ("double", "red") + self.label.styles.padding = 1, 1 + self.label.styles.margin = 2, 4 + + +if __name__ == "__main__": + app = StaticAndLabelApp() + app.run() diff --git a/python-textual/static_and_label.tcss b/python-textual/static_and_label.tcss new file mode 100644 index 0000000000..5152b4c141 --- /dev/null +++ b/python-textual/static_and_label.tcss @@ -0,0 +1,23 @@ +Static { + background: blue; + border: solid white; + padding: 1 1; + margin: 2 2; + text-align: center; +} + +#label_id { + color: black; + background: yellow; + border: solid black; + padding: 1 1; + margin: 2 4; +} + +.label_class { + color:black; + background: green; + border: dashed purple; + padding: 1 1; + margin: 2 6; +} \ No newline at end of file diff --git a/python-textual/static_and_label_tcss.py b/python-textual/static_and_label_tcss.py new file mode 100644 index 0000000000..9cf79c01c7 --- /dev/null +++ b/python-textual/static_and_label_tcss.py @@ -0,0 +1,24 @@ +from textual.app import App +from textual.widgets import Label, Static + + +class StaticAndLabelAppWithTCSS(App): + CSS_PATH = "static_and_label.tcss" + + def compose(self): + yield Static( + "I am a [bold red]Static[/bold red] widget!", + ) + yield Label( + "I am a [yellow italic]Label[/yellow italic] widget with an id!", + id="label_id", + ) + yield Label( + "I am a [yellow italic]Label[/yellow italic] widget with a CSS class!", + classes="label_class", + ) + + +if __name__ == "__main__": + app = StaticAndLabelAppWithTCSS() + app.run() diff --git a/python-textual/vertical_layout.py.py b/python-textual/vertical_layout.py.py new file mode 100644 index 0000000000..2eb0037d07 --- /dev/null +++ b/python-textual/vertical_layout.py.py @@ -0,0 +1,22 @@ +from textual.app import App +from textual.containers import Vertical +from textual.widgets import Static + +NUM_BOXES = 4 + +class VerticalLayoutExample(App): + def compose(self): + with Vertical(): + for i in range(NUM_BOXES): + static = Static(f"Static {i+1}") + static.styles.border = ("solid", "green") + yield static + + # for i in range(1, NUM_BOXES): + # static = Static(f"Static {i}") + # static.styles.border = ("solid", "green") + # yield static + +if __name__ == "__main__": + app = VerticalLayoutExample() + app.run() diff --git a/python-textual/vertical_layout.tcss b/python-textual/vertical_layout.tcss new file mode 100644 index 0000000000..2dc0a10796 --- /dev/null +++ b/python-textual/vertical_layout.tcss @@ -0,0 +1,3 @@ +Static { + border: solid green; +} \ No newline at end of file diff --git a/python-textual/vertical_layout_tcss.py.py b/python-textual/vertical_layout_tcss.py.py new file mode 100644 index 0000000000..230ac733da --- /dev/null +++ b/python-textual/vertical_layout_tcss.py.py @@ -0,0 +1,17 @@ +from textual.app import App +from textual.containers import Vertical +from textual.widgets import Static + +NUM_BOXES = 4 + +class VerticalLayoutExampleWithTCSS(App): + CSS_PATH="vertical_layout.tcss" + + def compose(self): + with Vertical(): + for i in range(NUM_BOXES): + yield Static(f"Static {i + 1}") + +if __name__ == "__main__": + app = VerticalLayoutExampleWithTCSS() + app.run() diff --git a/python-textual/vertical_scroll.py b/python-textual/vertical_scroll.py new file mode 100644 index 0000000000..e2b761959d --- /dev/null +++ b/python-textual/vertical_scroll.py @@ -0,0 +1,17 @@ +from textual.app import App +from textual.containers import VerticalScroll +from textual.widgets import Static + +NUM_BOXES = 20 + +class VerticalScrollExample(App): + CSS_PATH="vertical_layout.tcss" + + def compose(self): + with VerticalScroll(): + for i in range(NUM_BOXES): + yield Static(f"Static {i + 1}") + +if __name__ == "__main__": + app = VerticalScrollExample() + app.run() From 987a0d14991bd71e67530d12830de76f911fc7ff Mon Sep 17 00:00:00 2001 From: Leodanis Pozo Ramos Date: Wed, 19 Feb 2025 11:43:54 +0100 Subject: [PATCH 2/2] Format imports --- python-textual/buttons_and_inputs.py | 2 ++ python-textual/events.py | 2 ++ python-textual/grid.py | 2 ++ python-textual/grid_with_tcss.py | 2 ++ python-textual/hello_textual.py | 2 ++ python-textual/horizontal_layout.py | 4 +++- python-textual/horizontal_scroll.py | 4 +++- python-textual/layouts.py.py | 2 ++ python-textual/vertical_layout.py.py | 2 ++ python-textual/vertical_layout_tcss.py.py | 4 +++- python-textual/vertical_scroll.py | 4 +++- 11 files changed, 26 insertions(+), 4 deletions(-) diff --git a/python-textual/buttons_and_inputs.py b/python-textual/buttons_and_inputs.py index 12025fedb8..4627e47f54 100644 --- a/python-textual/buttons_and_inputs.py +++ b/python-textual/buttons_and_inputs.py @@ -1,6 +1,7 @@ from textual.app import App from textual.widgets import Button, Input + class ButtonsAndInputsApp(App): def compose(self): # Buttons @@ -18,6 +19,7 @@ def compose(self): tooltip="Digits only please!", ) + if __name__ == "__main__": app = ButtonsAndInputsApp() app.run() diff --git a/python-textual/events.py b/python-textual/events.py index 2d6393f6a3..e1c96b5d6c 100644 --- a/python-textual/events.py +++ b/python-textual/events.py @@ -2,6 +2,7 @@ from textual.app import App from textual.widgets import Button, Digits, Footer + class EventsApp(App): CSS_PATH = "events.tcss" BINDINGS = [ @@ -42,6 +43,7 @@ def on_button_pressed(self, event): # digits = self.query_one("#digits") # digits.update(f"{self.presses_count}") + if __name__ == "__main__": app = EventsApp() app.run() diff --git a/python-textual/grid.py b/python-textual/grid.py index cd2836c165..4b8219aba8 100644 --- a/python-textual/grid.py +++ b/python-textual/grid.py @@ -2,6 +2,7 @@ from textual.containers import Grid from textual.widgets import Static + class GridLayoutExample(App): def compose(self): grid = Grid() @@ -16,6 +17,7 @@ def compose(self): static.styles.height = "1fr" yield static + if __name__ == "__main__": app = GridLayoutExample() app.run() diff --git a/python-textual/grid_with_tcss.py b/python-textual/grid_with_tcss.py index 5a5f1053ef..d76333a457 100644 --- a/python-textual/grid_with_tcss.py +++ b/python-textual/grid_with_tcss.py @@ -2,6 +2,7 @@ from textual.containers import Grid from textual.widgets import Static + class GridLayoutWithTCSS(App): CSS_PATH = "grid.tcss" @@ -11,6 +12,7 @@ def compose(self): for col in range(4): yield Static(f"Static ({row=}, {col=})") + if __name__ == "__main__": app = GridLayoutWithTCSS() app.run() diff --git a/python-textual/hello_textual.py b/python-textual/hello_textual.py index eaad495b1a..e3c9a8a41f 100644 --- a/python-textual/hello_textual.py +++ b/python-textual/hello_textual.py @@ -1,10 +1,12 @@ from textual.app import App from textual.widgets import Static + class HelloTextualApp(App): def compose(self): yield Static("Hello, Textual!") + if __name__ == "__main__": app = HelloTextualApp() app.run() diff --git a/python-textual/horizontal_layout.py b/python-textual/horizontal_layout.py index 029066d2e0..d4456a8e95 100644 --- a/python-textual/horizontal_layout.py +++ b/python-textual/horizontal_layout.py @@ -4,15 +4,17 @@ NUM_BOXES = 4 + class HorizontalLayoutExample(App): def compose(self): with Horizontal(): for i in range(NUM_BOXES): static = Static(f"Static {i+1}") static.styles.border = ("solid", "green") - static.styles.width="10%" + static.styles.width = "10%" yield static + if __name__ == "__main__": app = HorizontalLayoutExample() app.run() diff --git a/python-textual/horizontal_scroll.py b/python-textual/horizontal_scroll.py index 9a11f1b522..301cfc4e5d 100644 --- a/python-textual/horizontal_scroll.py +++ b/python-textual/horizontal_scroll.py @@ -4,15 +4,17 @@ NUM_BOXES = 20 + class HorizontalScrollExample(App): def compose(self): with HorizontalScroll(): for i in range(NUM_BOXES): static = Static(f"Static {i+1}") static.styles.border = ("solid", "green") - static.styles.width="10%" + static.styles.width = "10%" yield static + if __name__ == "__main__": app = HorizontalScrollExample() app.run() diff --git a/python-textual/layouts.py.py b/python-textual/layouts.py.py index 12d2315013..a32572265b 100644 --- a/python-textual/layouts.py.py +++ b/python-textual/layouts.py.py @@ -8,6 +8,7 @@ NUM_BOXES = 12 + class NestedContainersExample(App): CSS_PATH = "layouts.tcss" @@ -31,6 +32,7 @@ def compose(self): id="docked-label", ) + if __name__ == "__main__": app = NestedContainersExample() app.run() diff --git a/python-textual/vertical_layout.py.py b/python-textual/vertical_layout.py.py index 2eb0037d07..e6669789c7 100644 --- a/python-textual/vertical_layout.py.py +++ b/python-textual/vertical_layout.py.py @@ -4,6 +4,7 @@ NUM_BOXES = 4 + class VerticalLayoutExample(App): def compose(self): with Vertical(): @@ -17,6 +18,7 @@ def compose(self): # static.styles.border = ("solid", "green") # yield static + if __name__ == "__main__": app = VerticalLayoutExample() app.run() diff --git a/python-textual/vertical_layout_tcss.py.py b/python-textual/vertical_layout_tcss.py.py index 230ac733da..c405e3085b 100644 --- a/python-textual/vertical_layout_tcss.py.py +++ b/python-textual/vertical_layout_tcss.py.py @@ -4,14 +4,16 @@ NUM_BOXES = 4 + class VerticalLayoutExampleWithTCSS(App): - CSS_PATH="vertical_layout.tcss" + CSS_PATH = "vertical_layout.tcss" def compose(self): with Vertical(): for i in range(NUM_BOXES): yield Static(f"Static {i + 1}") + if __name__ == "__main__": app = VerticalLayoutExampleWithTCSS() app.run() diff --git a/python-textual/vertical_scroll.py b/python-textual/vertical_scroll.py index e2b761959d..9abecda85b 100644 --- a/python-textual/vertical_scroll.py +++ b/python-textual/vertical_scroll.py @@ -4,14 +4,16 @@ NUM_BOXES = 20 + class VerticalScrollExample(App): - CSS_PATH="vertical_layout.tcss" + CSS_PATH = "vertical_layout.tcss" def compose(self): with VerticalScroll(): for i in range(NUM_BOXES): yield Static(f"Static {i + 1}") + if __name__ == "__main__": app = VerticalScrollExample() app.run()