diff --git a/src/textual/__main__.py b/src/textual/__main__.py index 3bf8ea235c..9da832d968 100644 --- a/src/textual/__main__.py +++ b/src/textual/__main__.py @@ -1,4 +1,4 @@ -from textual.demo import DemoApp +from textual.demo.demo_app import DemoApp if __name__ == "__main__": app = DemoApp() diff --git a/src/textual/demo.py b/src/textual/demo.py deleted file mode 100644 index 9a5acf176d..0000000000 --- a/src/textual/demo.py +++ /dev/null @@ -1,400 +0,0 @@ -from __future__ import annotations - -from importlib.metadata import version -from pathlib import Path -from typing import cast - -from rich import box -from rich.console import RenderableType -from rich.json import JSON -from rich.markdown import Markdown -from rich.markup import escape -from rich.pretty import Pretty -from rich.syntax import Syntax -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, ScrollableContainer -from textual.reactive import reactive -from textual.widgets import ( - Button, - DataTable, - Footer, - Header, - Input, - RichLog, - Static, - Switch, -) - -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("[magenta]Box Office"), - style="magenta", - justify="right", - no_wrap=True, -) -example_table.add_row( - "Dec 20, 2019", - "Star Wars: The Rise of Skywalker", - "$375,126,118", -) -example_table.add_row( - "May 25, 2018", - from_markup("[b]Solo[/]: A Star Wars Story"), - "$393,151,347", -) -example_table.add_row( - "Dec 15, 2017", - "Star Wars Ep. VIII: The Last Jedi", - 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"), - "$1,027,044,677", -) - - -WELCOME_MD = """ - -## Textual Demo - -**Welcome**! Textual is a framework for creating sophisticated applications with the terminal. -""" - - -RICH_MD = """ - -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). - -Here are some examples: -""" - -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: -""" - -DATA = { - "foo": [ - 3.1427, - ( - "Paul Atreides", - "Vladimir Harkonnen", - "Thufir Hawat", - "Gurney Halleck", - "Duncan Idaho", - ), - ], -} - -WIDGETS_MD = """ - -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. -- **Switch** A switch to toggle between states. -- **DataTable** A spreadsheet-like widget for navigating data. Cells may contain text or Rich renderables. -- **Tree** 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[/] -""" - - -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(ScrollableContainer): - pass - - -class Title(Static): - pass - - -class DarkSwitch(Horizontal): - def compose(self) -> ComposeResult: - yield Switch(value=self.app.dark) - yield Static("Dark mode toggle", classes="label") - - def on_mount(self) -> None: - self.watch(self.app, "dark", self.on_dark_change, init=False) - - def on_dark_change(self) -> None: - self.query_one(Switch).value = self.app.dark - - def on_switch_changed(self, event: Switch.Changed) -> None: - self.app.dark = event.value - - -class Welcome(Container): - ALLOW_MAXIMIZE = True - - def compose(self) -> ComposeResult: - yield Static(Markdown(WELCOME_MD)) - yield Button("Start", variant="success") - - def on_button_pressed(self, event: Button.Pressed) -> None: - app = cast(DemoApp, self.app) - app.add_note("[b magenta]Start!") - app.query_one(".location-first").scroll_visible(duration=0.5, top=True) - - -class OptionGroup(Container): - pass - - -class SectionTitle(Static): - pass - - -class Message(Static): - pass - - -class Version(Static): - def render(self) -> RenderableType: - return f"[b]v{version('textual')}" - - -class Sidebar(Container): - def compose(self) -> ComposeResult: - yield Title("Textual Demo") - yield OptionGroup(Message(MESSAGE), Version()) - yield DarkSwitch() - - -class AboveFold(Container): - pass - - -class Section(Container): - pass - - -class Column(Container): - pass - - -class TextContent(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: - app = cast(DemoApp, self.app) - app.query_one(self.reveal).scroll_visible(top=True, duration=0.5) - app.add_note(f"Scrolling to [b]{self.reveal}[/b]") - - -class LoginForm(Container): - ALLOW_MAXIMIZE = True - - 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 Window(Container): - pass - - -class SubTitle(Static): - pass - - -class DemoApp(App[None]): - CSS_PATH = "demo.tcss" - TITLE = "Textual Demo" - BINDINGS = [ - ("ctrl+b", "toggle_sidebar", "Sidebar"), - ("ctrl+t", "app.toggle_dark", "Toggle Dark mode"), - ("ctrl+s", "app.screenshot()", "Screenshot"), - ("f1", "app.toggle_class('RichLog', '-hidden')", "Notes"), - Binding("ctrl+q", "app.quit", "Quit", show=True), - ] - - show_sidebar = reactive(False) - - def add_note(self, renderable: RenderableType) -> None: - self.query_one(RichLog).write(renderable) - - def compose(self) -> ComposeResult: - example_css = Path(self.css_path[0]).read_text() - yield Container( - Sidebar(classes="-hidden"), - Header(show_clock=False), - RichLog(classes="-hidden", wrap=False, highlight=True, markup=True), - Body( - QuickAccess( - LocationLink("TOP", ".location-top"), - LocationLink("Widgets", ".location-widgets"), - LocationLink("Rich content", ".location-rich"), - LocationLink("CSS", ".location-css"), - ), - AboveFold(Welcome(), classes="location-top"), - Column( - Section( - SectionTitle("Widgets"), - TextContent(Markdown(WIDGETS_MD)), - LoginForm(), - DataTable(), - ), - classes="location-widgets location-first", - ), - Column( - Section( - SectionTitle("Rich"), - TextContent(Markdown(RICH_MD)), - SubTitle("Pretty Printed data (try resizing the terminal)"), - Static(Pretty(DATA, indent_guides=True), classes="pretty pad"), - SubTitle("JSON"), - Window(Static(JSON(JSON_EXAMPLE), expand=True), classes="pad"), - SubTitle("Tables"), - Static(example_table, classes="table pad"), - ), - classes="location-rich", - ), - Column( - Section( - SectionTitle("CSS"), - TextContent(Markdown(CSS_MD)), - Window( - Static( - Syntax( - example_css, - "css", - theme="material", - line_numbers=True, - ), - expand=True, - ) - ), - ), - classes="location-css", - ), - ), - ) - yield Footer() - - def action_open_link(self, link: str) -> None: - self.app.bell() - import webbrowser - - webbrowser.open(link) - - def action_toggle_sidebar(self) -> None: - sidebar = self.query_one(Sidebar) - self.set_focus(None) - 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) - 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)]) - self.query_one("Welcome Button", Button).focus() - - def action_screenshot(self, filename: str | None = None, path: str = "./") -> None: - """Save an SVG "screenshot". This action will save an SVG file containing the current contents of the screen. - - Args: - filename: Filename of screenshot, or None to auto-generate. - path: Path to directory. - """ - self.bell() - path = self.save_screenshot(filename, path) - message = f"Screenshot saved to [bold green]'{escape(str(path))}'[/]" - self.add_note(Text.from_markup(message)) - self.notify(message) - - -app = DemoApp() -if __name__ == "__main__": - app.run() diff --git a/src/textual/demo.tcss b/src/textual/demo.tcss deleted file mode 100644 index 4febd734be..0000000000 --- a/src/textual/demo.tcss +++ /dev/null @@ -1,271 +0,0 @@ -* { - transition: background 500ms in_out_cubic, color 500ms in_out_cubic; -} - -Screen { - layers: base overlay notes notifications; - overflow: hidden; - &:inline { - height: 50vh; - } - &.-maximized-view { - overflow: auto; - } -} - - -Notification { - dock: bottom; - layer: notification; - width: auto; - margin: 2 4; - padding: 1 2; - background: $background; - color: $text; - height: auto; - -} - -Sidebar { - width: 40; - background: $panel; - transition: offset 500ms in_out_cubic; - layer: overlay; - -} - -Sidebar:focus-within { - offset: 0 0 !important; -} - -Sidebar.-hidden { - offset-x: -100%; -} - -Sidebar Title { - background: $boost; - color: $secondary; - padding: 2 4; - border-right: vkey $background; - dock: top; - text-align: center; - text-style: bold; -} - - -OptionGroup { - background: $boost; - color: $text; - 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: scroll; - width: 100%; - background: $surface; - -} - -AboveFold { - width: 100%; - height: 100%; - align: center middle; -} - -Welcome { - background: $boost; - height: auto; - max-width: 100; - min-width: 40; - border: wide $primary; - padding: 1 2; - margin: 1 2; - box-sizing: border-box; -} - -Welcome Button { - width: 100%; - margin-top: 1; -} - -Column { - height: auto; - min-height: 100vh; - align: center top; - overflow: hidden; -} - - -DarkSwitch { - background: $panel; - padding: 1; - dock: bottom; - height: auto; - border-right: vkey $background; -} - -DarkSwitch .label { - width: 1fr; - padding: 1 2; - color: $text-muted; -} - -DarkSwitch Switch { - background: $boost; - dock: left; -} - - -Screen>Container { - height: 100%; - overflow: hidden; -} - -RichLog { - background: $surface; - color: $text; - height: 50vh; - dock: bottom; - layer: notes; - border-top: hkey $primary; - offset-y: 0; - transition: offset 400ms in_out_cubic; - padding: 0 1 1 1; -} - - -RichLog:focus { - offset: 0 0 !important; -} - -RichLog.-hidden { - offset-y: 100%; -} - - - -Section { - height: auto; - min-width: 40; - margin: 1 2 4 2; - -} - -SectionTitle { - padding: 1 2; - background: $boost; - text-align: center; - text-style: bold; -} - -SubTitle { - padding-top: 1; - border-bottom: heavy $panel; - color: $text; - text-style: bold; -} - -TextContent { - margin: 1 0; -} - -QuickAccess { - width: 30; - dock: left; - -} - -LocationLink { - margin: 1 0 0 1; - height: 1; - padding: 1 2; - background: $boost; - color: $text; - box-sizing: content-box; - content-align: center middle; -} - -LocationLink:hover { - background: $accent; - color: $text; - text-style: bold; -} - - -.pad { - margin: 1 0; -} - -DataTable { - height: 16; - max-height: 16; -} - - -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; -} - -Message { - margin: 0 1; - -} - - -Tree { - margin: 1 0; -} - - -Window { - background: $boost; - overflow: auto; - height: auto; - max-height: 16; -} - -Window>Static { - width: auto; -} - - -Version { - color: $text-disabled; - dock: bottom; - text-align: center; - padding: 1; -} diff --git a/src/textual/demo2/__main__.py b/src/textual/demo/__main__.py similarity index 100% rename from src/textual/demo2/__main__.py rename to src/textual/demo/__main__.py diff --git a/src/textual/demo2/data.py b/src/textual/demo/data.py similarity index 100% rename from src/textual/demo2/data.py rename to src/textual/demo/data.py diff --git a/src/textual/demo2/demo_app.py b/src/textual/demo/demo_app.py similarity index 74% rename from src/textual/demo2/demo_app.py rename to src/textual/demo/demo_app.py index 28307d30b1..d3be0a33c2 100644 --- a/src/textual/demo2/demo_app.py +++ b/src/textual/demo/demo_app.py @@ -1,8 +1,8 @@ from textual.app import App from textual.binding import Binding -from textual.demo2.home import HomeScreen -from textual.demo2.projects import ProjectsScreen -from textual.demo2.widgets import WidgetsScreen +from textual.demo.home import HomeScreen +from textual.demo.projects import ProjectsScreen +from textual.demo.widgets import WidgetsScreen class DemoApp(App): @@ -40,4 +40,10 @@ class DemoApp(App): "widgets", tooltip="Test the builtin widgets", ), + Binding( + "ctrl+s", + "app.screenshot", + "Screenshot", + tooltip="Save an SVG 'screenshot' of the current screen", + ), ] diff --git a/src/textual/demo2/home.py b/src/textual/demo/home.py similarity index 96% rename from src/textual/demo2/home.py rename to src/textual/demo/home.py index 53611c7aaf..2ce64ddd95 100644 --- a/src/textual/demo2/home.py +++ b/src/textual/demo/home.py @@ -6,7 +6,7 @@ from textual import work from textual.app import ComposeResult from textual.containers import Horizontal, Vertical, VerticalScroll -from textual.demo2.page import PageScreen +from textual.demo.page import PageScreen from textual.reactive import reactive from textual.widgets import Collapsible, Digits, Footer, Label, Markdown @@ -41,14 +41,14 @@ ## Textual interfaces are *snappy* Even the most modern of web apps can leave the user waiting hundreds of milliseconds or more for a response. -Given their low graphical requirements, Textual interfaces can be far more responsive—no waiting required. +Given their low graphical requirements, Textual interfaces can be far more responsive — no waiting required. ## Reward repeated use Use the mouse to explore, but Textual apps are keyboard-centric and reward repeated use. An experience user can operate a Textual app far faster than their web / GUI counterparts. ## Command palette -A builtin command palette with fuzzy searching, puts powerful commands at your fingertips. +A builtin command palette with fuzzy searching puts powerful commands at your fingertips. **Try it:** Press **ctrl+p** now. @@ -57,6 +57,12 @@ API_MD = """\ A modern Python API from the developer of [Rich](https://github.com/Textualize/rich). +```python +# Start building! +import textual + +``` + Well documented, typed, and intuitive. Textual's API is accessible to Python developers of all skill levels. diff --git a/src/textual/demo2/page.py b/src/textual/demo/page.py similarity index 100% rename from src/textual/demo2/page.py rename to src/textual/demo/page.py diff --git a/src/textual/demo2/projects.py b/src/textual/demo/projects.py similarity index 99% rename from src/textual/demo2/projects.py rename to src/textual/demo/projects.py index e5fa217491..d352fbcbd3 100644 --- a/src/textual/demo2/projects.py +++ b/src/textual/demo/projects.py @@ -4,7 +4,7 @@ from textual.app import ComposeResult from textual.binding import Binding from textual.containers import Center, Horizontal, ItemGrid, Vertical, VerticalScroll -from textual.demo2.page import PageScreen +from textual.demo.page import PageScreen from textual.widgets import Footer, Label, Link, Markdown, Static diff --git a/src/textual/demo2/widgets.py b/src/textual/demo/widgets.py similarity index 93% rename from src/textual/demo2/widgets.py rename to src/textual/demo/widgets.py index 597d862059..bad0b6ad71 100644 --- a/src/textual/demo2/widgets.py +++ b/src/textual/demo/widgets.py @@ -8,8 +8,8 @@ from textual import containers from textual.app import ComposeResult -from textual.demo2.data import COUNTRIES -from textual.demo2.page import PageScreen +from textual.demo.data import COUNTRIES +from textual.demo.page import PageScreen from textual.reactive import reactive, var from textual.suggester import SuggestFromList from textual.widgets import ( @@ -96,6 +96,8 @@ def compose(self) -> ComposeResult: class Checkboxes(containers.VerticalGroup): + """Demonstrates Checkboxes.""" + DEFAULT_CLASSES = "column" DEFAULT_CSS = """ Checkboxes { @@ -137,6 +139,8 @@ def compose(self) -> ComposeResult: class Datatables(containers.VerticalGroup): + """Demonstrates DataTables.""" + DEFAULT_CLASSES = "column" DATATABLES_MD = """\ ## Datatables @@ -170,6 +174,8 @@ def on_mount(self) -> None: class Inputs(containers.VerticalGroup): + """Demonstrates Inputs.""" + DEFAULT_CLASSES = "column" INPUTS_MD = """\ ## Inputs and MaskedInputs @@ -223,6 +229,8 @@ def compose(self) -> ComposeResult: class ListViews(containers.VerticalGroup): + """Demonstrates List Views and Option Lists.""" + DEFAULT_CLASSES = "column" LISTS_MD = """\ ## List Views and Option Lists @@ -258,6 +266,8 @@ def compose(self) -> ComposeResult: class Logs(containers.VerticalGroup): + """Demonstrates Logs.""" + DEFAULT_CLASSES = "column" LOGS_MD = """\ ## Logs and Rich Logs @@ -334,6 +344,7 @@ def on_mount(self) -> None: self.set_interval(1, self.update_rich_log) def update_log(self) -> None: + """Update the Log with new content.""" if not self.screen.can_view(self) or not self.screen.is_active: return self.log_count += 1 @@ -343,6 +354,7 @@ def update_log(self) -> None: log.write_line(f"fear[{line_no}] = {line!r}") def update_rich_log(self) -> None: + """Update the Rich Log with content.""" if not self.screen.can_view(self) or not self.screen.is_active: return rich_log = self.query_one(RichLog) @@ -367,6 +379,8 @@ def update_rich_log(self) -> None: class Sparklines(containers.VerticalGroup): + """Demonstrates sparklines.""" + DEFAULT_CLASSES = "column" LOGS_MD = """\ ## Sparklines @@ -380,12 +394,12 @@ class Sparklines(containers.VerticalGroup): Sparkline { width: 1fr; margin: 1; - & #first > .sparkline--min-color { color: $success; } - & #first > .sparkline--max-color { color: $warning; } - & #second > .sparkline--min-color { color: $warning; } - & #second > .sparkline--max-color { color: $error; } - & #third > .sparkline--min-color { color: $primary; } - & #third > .sparkline--max-color { color: $accent; } + &#first > .sparkline--min-color { color: $success; } + &#first > .sparkline--max-color { color: $warning; } + &#second > .sparkline--min-color { color: $warning; } + &#second > .sparkline--max-color { color: $error; } + &#third > .sparkline--min-color { color: $primary; } + &#third > .sparkline--max-color { color: $accent; } } } @@ -407,9 +421,10 @@ def compose(self) -> ComposeResult: ) def on_mount(self) -> None: - self.set_interval(0.1, self.update_sparks) + self.set_interval(0.2, self.update_sparks) def update_sparks(self) -> None: + """Update the sparks data.""" if not self.screen.can_view(self) or not self.screen.is_active: return self.count += 1 @@ -418,6 +433,8 @@ def update_sparks(self) -> None: class WidgetsScreen(PageScreen): + """The Widgets screen""" + CSS = """ WidgetsScreen { align-horizontal: center;