From 7b7dd9d811196a4a91f06b5a759cab986abe291c Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Tue, 25 Oct 2022 07:47:13 -0500 Subject: [PATCH] init --- LICENSE.txt | 1 + README.md | 21 ++ pyproject.toml | 73 +++++++ tests/__init__.py | 3 + tests/tui.py | 2 + textual_tutorial/__about__.py | 4 + textual_tutorial/__init__.py | 3 + .../__pycache__/__init__.cpython-310.pyc | Bin 0 -> 158 bytes .../__pycache__/tui.cpython-310.pyc | Bin 0 -> 6555 bytes textual_tutorial/tui.css | 56 ++++++ textual_tutorial/tui.py | 189 ++++++++++++++++++ 11 files changed, 352 insertions(+) create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 pyproject.toml create mode 100644 tests/__init__.py create mode 100644 tests/tui.py create mode 100644 textual_tutorial/__about__.py create mode 100644 textual_tutorial/__init__.py create mode 100644 textual_tutorial/__pycache__/__init__.cpython-310.pyc create mode 100644 textual_tutorial/__pycache__/tui.cpython-310.pyc create mode 100644 textual_tutorial/tui.css create mode 100644 textual_tutorial/tui.py diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1 @@ + diff --git a/README.md b/README.md new file mode 100644 index 0000000..4e5400e --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# textual-tutorial + +[![PyPI - Version](https://img.shields.io/pypi/v/textual-tutorial.svg)](https://pypi.org/project/textual-tutorial) +[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/textual-tutorial.svg)](https://pypi.org/project/textual-tutorial) + +----- + +**Table of Contents** + +- [Installation](#installation) +- [License](#license) + +## Installation + +```console +pip install textual-tutorial +``` + +## License + +`textual-tutorial` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..97b0147 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,73 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "textual-tutorial" +description = '' +readme = "README.md" +requires-python = ">=3.7" +license = "MIT" +keywords = [] +authors = [ + { name = "Waylon S. Walker", email = "waylon@waylonwalker.com" }, +] +classifiers = [ + "Development Status :: 4 - Beta", + "Programming Language :: Python", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", +] +dependencies = ['textual'] +dynamic = ["version"] + +[project.scripts] +tutorial = 'textual_tutorial.tui:tui' + +[project.urls] +Documentation = "https://github.com/unknown/textual-tutorial#readme" +Issues = "https://github.com/unknown/textual-tutorial/issues" +Source = "https://github.com/unknown/textual-tutorial" + +[tool.hatch.version] +path = "textual_tutorial/__about__.py" + + +[project.optional-dependencies] +dev = [ + 'textual[dev]', + "pytest", + "pytest-cov", +] + +[tool.hatch.envs.default] +dependencies = [ + 'textual[dev]', + "pytest", + "pytest-cov", +] +[tool.hatch.envs.default.scripts] +cov = "pytest --cov-report=term-missing --cov-config=pyproject.toml --cov=textual_tutorial --cov=tests {args}" +no-cov = "cov --no-cov {args}" + +[[tool.hatch.envs.test.matrix]] +python = ["37", "38", "39", "310", "311"] + +[tool.coverage.run] +branch = true +parallel = true +omit = [ + "textual_tutorial/__about__.py", +] + +[tool.coverage.report] +exclude_lines = [ + "no cov", + "if __name__ == .__main__.:", + "if TYPE_CHECKING:", +] diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..8ecc75f --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2022-present Waylon S. Walker +# +# SPDX-License-Identifier: MIT diff --git a/tests/tui.py b/tests/tui.py new file mode 100644 index 0000000..d4b1732 --- /dev/null +++ b/tests/tui.py @@ -0,0 +1,2 @@ +def tui(): + print("running") diff --git a/textual_tutorial/__about__.py b/textual_tutorial/__about__.py new file mode 100644 index 0000000..fe60eb5 --- /dev/null +++ b/textual_tutorial/__about__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2022-present Waylon S. Walker +# +# SPDX-License-Identifier: MIT +__version__ = '0.0.1' diff --git a/textual_tutorial/__init__.py b/textual_tutorial/__init__.py new file mode 100644 index 0000000..8ecc75f --- /dev/null +++ b/textual_tutorial/__init__.py @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2022-present Waylon S. Walker +# +# SPDX-License-Identifier: MIT diff --git a/textual_tutorial/__pycache__/__init__.cpython-310.pyc b/textual_tutorial/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f17428ca8cb5c4fcb0113bbf0e46905f5f4e89cd GIT binary patch literal 158 zcmd1j<>g`kf>%>RlZ%1$V-N=!FakLaKwQiMBvKfH88jLFRx%WUgb~CqNBxZa+*JM2 z`0~V@?9}pN{q)Qd{gTv*lG4N+-ICIh{G!an9GE~nTp&I^GcU6wK3=b&@)n0pZhlH> PPO2Tq=wc=y!NLFl7Q`n* literal 0 HcmV?d00001 diff --git a/textual_tutorial/__pycache__/tui.cpython-310.pyc b/textual_tutorial/__pycache__/tui.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..de8659b3f06387fc67135405c8844606bea5cfdf GIT binary patch literal 6555 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 literal 0 HcmV?d00001 diff --git a/textual_tutorial/tui.css b/textual_tutorial/tui.css new file mode 100644 index 0000000..7832c0d --- /dev/null +++ b/textual_tutorial/tui.css @@ -0,0 +1,56 @@ +Stopwatch { + layout: horizontal; + background: $boost; + height: 5; + padding: 1; + margin: 1; +} + +Stopwatch.active { + background: red; + background: $background-lighten-3; +} + +TimeDisplay { + content-align: center middle; + text-opacity: 60%; + height: 3; +} + +Button { + width: 16; +} + +#start { + dock: left; +} + +#stop { + dock: left; + display: none; +} + +#reset { + dock: right; +} +.started { + text-style: bold; + background: $success; + color: $text; +} + +.started TimeDisplay { + text-opacity: 100%; +} + +.started #start { + display: none +} + +.started #stop { + display: block +} + +.started #reset { + visibility: hidden +} diff --git a/textual_tutorial/tui.py b/textual_tutorial/tui.py new file mode 100644 index 0000000..5fcb40d --- /dev/null +++ b/textual_tutorial/tui.py @@ -0,0 +1,189 @@ +from pathlib import Path +from textual.app import App, ComposeResult +from textual.css.query import NoMatches +from textual.widgets import Header, Footer + +from textual.containers import Container +from textual.widgets import Button, Header, Footer, Static + +from textual.reactive import reactive +from time import monotonic + + +class TimeDisplay(Static): + """A widget to display elapsed time.""" + + start_time = reactive(monotonic) + time = reactive(0.0) + total = reactive(0.0) + + def on_mount(self) -> None: + """Event handler called when widget is added to the app.""" + self.update_timer = self.set_interval(1 / 60, self.update_time, pause=True) + + def update_time(self) -> None: + """Method to update the time to the current time.""" + self.time = self.total + (monotonic() - self.start_time) + + def watch_time(self, time: float) -> None: + """Called when the time attribute changes.""" + minutes, seconds = divmod(time, 60) + hours, minutes = divmod(minutes, 60) + self.update(f"{hours:02,.0f}:{minutes:02.0f}:{seconds:05.2f}") + + def start(self) -> None: + """Method to start (or resume) time updating.""" + self.start_time = monotonic() + self.update_timer.resume() + + def stop(self): + """Method to stop the time display updating.""" + self.update_timer.pause() + self.total += monotonic() - self.start_time + self.time = self.total + + def reset(self): + """Method to reset the time display to zero.""" + self.total = 0 + self.time = 0 + + +class Stopwatch(Static): + """A stopwatch widget.""" + + def on_button_pressed(self, event: Button.Pressed) -> None: + """Event handler called when a button is pressed.""" + button_id = event.button.id + time_display = self.query_one(TimeDisplay) + if button_id == "start": + time_display.start() + self.add_class("started") + elif button_id == "stop": + time_display.stop() + self.remove_class("started") + elif button_id == "reset": + time_display.reset() + + def compose(self) -> ComposeResult: + """Create child widgets of a stopwatch.""" + yield Button("Start", id="start", variant="success") + yield Button("Stop", id="stop", variant="error") + yield Button("Reset", id="reset") + yield TimeDisplay("00:00:00.00") + + +class StopwatchApp(App): + """A Textual app to manage stopwatches.""" + + CSS_PATH = Path("__file__").parent / "tui.css" + BINDINGS = [ + ("q", "quit", "Quit"), + ("d", "toggle_dark", "Toggle dark mode"), + ("a", "add_stopwatch", "Add"), + ("r", "remove_stopwatch", "Remove"), + ("R", "reset", "Reset"), + ("j", "next", "Next"), + ("j", "next", "Next"), + ("k", "prev", "Prev"), + ("space", "toggle", "Toggle"), + ] + + def on_mount(self): + + try: + self.query_one("Stopwatch").add_class("active") + except NoMatches: + ... + + def compose(self) -> ComposeResult: + """Create child widgets for the app.""" + yield Header() + yield Container( + # Stopwatch(), + # Stopwatch(), + # Stopwatch(), + id="timers", + ) + yield Footer() + + def action_next(self): + self.activate(1) + + def action_prev(self): + self.activate(-1) + + def action_toggle(self): + try: + active = self.query_one("Stopwatch.active") + except NoMatches: + return + if "started" in active.classes: + active.query_one("#stop").press() + else: + active.query_one("#start").press() + + def action_reset(self): + try: + active = self.query_one("Stopwatch.active") + active.query_one("#reset").press() + except NoMatches: + ... + + def activate(self, n=1): + try: + active = self.query_one("Stopwatch.active") + except NoMatches: + return + stopwatches = self.query("Stopwatch").nodes + active_index = stopwatches.index(active) + next_index = active_index + n + if next_index > len(stopwatches) - 1: + next_index = 0 + if next_index < 0: + next_index = len(stopwatches) - 1 + active.remove_class("active") + stopwatches[next_index].add_class("active") + + def action_add_stopwatch(self) -> None: + """An action to add a timer.""" + new_stopwatch = Stopwatch() + try: + active = self.query_one("Stopwatch.active") + active.remove_class("active") + except NoMatches: + ... + new_stopwatch.add_class("active") + self.query_one("#timers").mount(new_stopwatch) + new_stopwatch.scroll_visible() + + def action_remove_stopwatch(self) -> None: + """Called to remove a timer.""" + timers = self.query("Stopwatch") + if timers: + timers.last().remove() + + def action_toggle_dark(self) -> None: + """An action to toggle dark mode.""" + self.dark = not self.dark + self.log("going dark") + + +def tui(): + + from textual.features import parse_features + import os + import sys + + dev = "--dev" in sys.argv + features = set(parse_features(os.environ.get("TEXTUAL", ""))) + if dev: + features.add("debug") + features.add("devtools") + + os.environ["TEXTUAL"] = ",".join(sorted(features)) + app = StopwatchApp() + app.run() + + +if __name__ == "__main__": + tui()