This commit is contained in:
Waylon Walker 2022-10-25 07:47:13 -05:00
commit 7b7dd9d811
No known key found for this signature in database
GPG key ID: 66E2BF2B4190EFE4
11 changed files with 352 additions and 0 deletions

1
LICENSE.txt Normal file
View file

@ -0,0 +1 @@

21
README.md Normal file
View file

@ -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.

73
pyproject.toml Normal file
View file

@ -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:",
]

3
tests/__init__.py Normal file
View file

@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: 2022-present Waylon S. Walker <waylon@waylonwalker.com>
#
# SPDX-License-Identifier: MIT

2
tests/tui.py Normal file
View file

@ -0,0 +1,2 @@
def tui():
print("running")

View file

@ -0,0 +1,4 @@
# SPDX-FileCopyrightText: 2022-present Waylon S. Walker <waylon@waylonwalker.com>
#
# SPDX-License-Identifier: MIT
__version__ = '0.0.1'

View file

@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: 2022-present Waylon S. Walker <waylon@waylonwalker.com>
#
# SPDX-License-Identifier: MIT

Binary file not shown.

Binary file not shown.

56
textual_tutorial/tui.css Normal file
View file

@ -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
}

189
textual_tutorial/tui.py Normal file
View file

@ -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()