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..4627e47f54 --- /dev/null +++ b/python-textual/buttons_and_inputs.py @@ -0,0 +1,25 @@ +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..e1c96b5d6c --- /dev/null +++ b/python-textual/events.py @@ -0,0 +1,49 @@ +# 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..4b8219aba8 --- /dev/null +++ b/python-textual/grid.py @@ -0,0 +1,23 @@ +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..d76333a457 --- /dev/null +++ b/python-textual/grid_with_tcss.py @@ -0,0 +1,18 @@ +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..e3c9a8a41f --- /dev/null +++ b/python-textual/hello_textual.py @@ -0,0 +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 new file mode 100644 index 0000000000..d4456a8e95 --- /dev/null +++ b/python-textual/horizontal_layout.py @@ -0,0 +1,20 @@ +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..301cfc4e5d --- /dev/null +++ b/python-textual/horizontal_scroll.py @@ -0,0 +1,20 @@ +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..a32572265b --- /dev/null +++ b/python-textual/layouts.py.py @@ -0,0 +1,38 @@ +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..e6669789c7 --- /dev/null +++ b/python-textual/vertical_layout.py.py @@ -0,0 +1,24 @@ +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..c405e3085b --- /dev/null +++ b/python-textual/vertical_layout_tcss.py.py @@ -0,0 +1,19 @@ +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..9abecda85b --- /dev/null +++ b/python-textual/vertical_scroll.py @@ -0,0 +1,19 @@ +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()