calculator example

This commit is contained in:
Will McGugan
2021-07-14 20:08:27 +01:00
parent b665ab330e
commit caf62a97da
15 changed files with 726 additions and 110 deletions

View File

@@ -5,7 +5,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
## [Unreleased] - yyyy-mm-dd
## [0.1.7] - 2021-07-14
Here we write upgrading notes for brands. It's a team effort to make them as
straightforward as possible.
### Changed
- Added functionality to calculator example.
- Scrollview now shows scrollbars automatically
- New handler system for messages that doesn't require inheritance
- Improved traceback handling

View File

@@ -1,17 +1,27 @@
from decimal import Decimal
from rich.align import Align
from rich.console import Console, ConsoleOptions, RenderResult
from rich.console import Console, ConsoleOptions, RenderResult, RenderableType
from rich.padding import Padding
from rich.text import Text
from textual.app import App
from textual import events
from textual.message import Message
from textual.reactive import Reactive
from textual.view import View
from textual.widgets import Button, Static
from textual.widget import Widget
from textual.widgets import Button
from textual.layouts.grid import GridLayout
try:
from pyfiglet import Figlet
except ImportError:
print("Please install pyfiglet to run this example")
import sys
sys.exit()
class FigletText:
@@ -24,10 +34,8 @@ class FigletText:
self, console: Console, options: ConsoleOptions
) -> RenderResult:
size = min(options.max_width / 2, options.max_height)
text = self.text
if size < 4:
yield Text(text, style="bold")
yield Text(self.text, style="bold")
else:
if size < 7:
font_name = "mini"
@@ -37,42 +45,148 @@ class FigletText:
font_name = "standard"
else:
font_name = "big"
font = Figlet(font=font_name)
yield Text(font.renderText(text).rstrip("\n"), style="bold")
font = Figlet(font=font_name, width=options.max_width)
yield Text(font.renderText(self.text).rstrip("\n"), style="bold")
class Numbers(Widget):
"""The digital display of the calculator."""
value: Reactive[str] = Reactive("0")
def render(self) -> RenderableType:
return Padding(
Align.right(FigletText(self.value), vertical="middle"),
(0, 1),
style="white on rgb(51,51,51)",
)
class CalculatorApp(App):
async def on_startup(self, event: events.Startup) -> None:
"""A working calculator app."""
async def on_load(self, event: events.Load) -> None:
"""Sent when the app starts, but before displaying anything."""
self.left = Decimal("0")
self.right = Decimal("0")
self.value = ""
self.operator = "+"
self.numbers = Numbers()
def make_button(text: str, style: str) -> Button:
"""Create a button with the given Figlet label."""
return Button(FigletText(text), style=style, name=text)
dark = "white on rgb(51,51,51)"
light = "black on rgb(165,165,165)"
yellow = "white on rgb(255,159,7)"
button_styles = {
"AC": light,
"C": light,
"+/-": light,
"%": light,
"/": yellow,
"X": yellow,
"-": yellow,
"+": yellow,
"=": yellow,
}
# Make all the buttons
self.buttons = {
name: make_button(name, button_styles.get(name, dark))
for name in "+/-,%,/,7,8,9,X,4,5,6,-,1,2,3,+,.,=".split(",")
}
self.zero = make_button("0", dark)
self.ac = make_button("AC", light)
self.c = make_button("C", light)
self.c.visible = False
async def on_startup(self, event: events.Startup) -> None:
"""Sent when the app has gone full screen."""
# Create the layout which defines where our widgets will go
layout = GridLayout(gap=(2, 1), gutter=1, align=("center", "center"))
await self.push_view(View(layout=layout))
# Create rows / columns / areas
layout.add_column("col", max_size=30, repeat=4)
layout.add_row("numbers")
layout.add_row("numbers", max_size=15)
layout.add_row("row", max_size=15, repeat=5)
layout.add_areas(
numbers="col1-start|col4-end,numbers", zero="col1-start|col2-end,row5"
clear="col1,row1",
numbers="col1-start|col4-end,numbers",
zero="col1-start|col2-end,row5",
)
def make_button(text: str) -> Button:
return Button(FigletText(text), style="white on rgb(51,51,51)")
buttons = {
name: make_button(name)
for name in "AC,+/-,%,/,7,8,9,X,4,5,6,-,1,2,3,+,.,=".split(",")
}
for name in ("AC", "+/-", "%"):
buttons[name].style = "black on rgb(165,165,165)"
for name in "/X-+=":
buttons[name].style = "white on rgb(255,159,7)"
numbers = Align.right(FigletText("0"), vertical="middle")
# Place out widgets in to the layout
layout.place(clear=self.c)
layout.place(
*buttons.values(),
numbers=Static(numbers, padding=(0, 1), style="white on rgb(51,51,51)"),
zero=make_button("0"),
*self.buttons.values(), clear=self.ac, numbers=self.numbers, zero=self.zero
)
async def message_button_pressed(self, message: Message) -> None:
"""A message sent by the button widget"""
assert isinstance(message.sender, Button)
button_name = message.sender.name
def do_math() -> bool:
operator = self.operator
right = self.right
if operator == "+":
self.left += right
elif operator == "-":
self.left -= right
elif operator == "/":
try:
self.left /= right
except ZeroDivisionError:
self.numbers.value = "Error"
return False
elif operator == "X":
self.left *= right
return True
if button_name.isdigit():
self.value = self.value.lstrip("0") + button_name
self.numbers.value = self.value
elif button_name == "+/-":
self.value = str(Decimal(self.value or "0") * -1)
self.numbers.value = self.value
elif button_name == "%":
self.value = str(Decimal(self.value or "0") / Decimal(100))
self.numbers.value = self.value
elif button_name == ".":
if "." not in self.value:
self.value += "."
self.numbers.value = self.value
elif button_name == "AC":
self.value = ""
self.left = self.right = Decimal(0)
self.operator = "+"
self.numbers.value = "0"
elif button_name == "C":
self.value = ""
self.numbers.value = "0"
elif button_name in ("+", "-", "/", "X"):
self.right = Decimal(self.value or "0")
if do_math():
self.numbers.value = str(self.left)
self.value = ""
self.operator = button_name
elif button_name == "=":
if self.value:
self.right = Decimal(self.value or "0")
if do_math():
self.numbers.value = str(self.left)
self.value = ""
show_ac = self.value in ("", "0") and self.numbers.value == "0"
self.c.visible = not show_ac
self.ac.visible = show_ac
CalculatorApp.run(title="Calculator Test")

386
poetry.lock generated
View File

@@ -6,6 +6,17 @@ category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "astunparse"
version = "1.6.3"
description = "An AST unparser for Python"
category = "dev"
optional = false
python-versions = "*"
[package.dependencies]
six = ">=1.6.1,<2.0"
[[package]]
name = "atomicwrites"
version = "1.4.0"
@@ -50,6 +61,14 @@ typing-extensions = ">=3.7.4"
colorama = ["colorama (>=0.4.3)"]
d = ["aiohttp (>=3.3.2)", "aiohttp-cors"]
[[package]]
name = "cached-property"
version = "1.5.2"
description = "A decorator for caching properties in classes."
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "click"
version = "8.0.1"
@@ -92,6 +111,20 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
[package.extras]
toml = ["toml"]
[[package]]
name = "ghp-import"
version = "2.0.1"
description = "Copy your docs directly to the gh-pages branch."
category = "dev"
optional = false
python-versions = "*"
[package.dependencies]
python-dateutil = ">=2.8.1"
[package.extras]
dev = ["twine", "markdown", "flake8"]
[[package]]
name = "importlib-metadata"
version = "4.6.1"
@@ -117,6 +150,128 @@ category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "jinja2"
version = "3.0.1"
description = "A very fast and expressive template engine."
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
MarkupSafe = ">=2.0"
[package.extras]
i18n = ["Babel (>=2.7)"]
[[package]]
name = "markdown"
version = "3.3.4"
description = "Python implementation of Markdown."
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
[package.extras]
testing = ["coverage", "pyyaml"]
[[package]]
name = "markupsafe"
version = "2.0.1"
description = "Safely add untrusted strings to HTML/XML markup."
category = "dev"
optional = false
python-versions = ">=3.6"
[[package]]
name = "mergedeep"
version = "1.3.4"
description = "A deep merge function for 🐍."
category = "dev"
optional = false
python-versions = ">=3.6"
[[package]]
name = "mkdocs"
version = "1.2.1"
description = "Project documentation with Markdown."
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
click = ">=3.3"
ghp-import = ">=1.0"
importlib-metadata = ">=3.10"
Jinja2 = ">=2.10.1"
Markdown = ">=3.2.1"
mergedeep = ">=1.3.4"
packaging = ">=20.5"
PyYAML = ">=3.10"
pyyaml-env-tag = ">=0.1"
watchdog = ">=2.0"
[package.extras]
i18n = ["babel (>=2.9.0)"]
[[package]]
name = "mkdocs-autorefs"
version = "0.2.1"
description = "Automatically link across pages in MkDocs."
category = "dev"
optional = false
python-versions = ">=3.6,<4.0"
[package.dependencies]
Markdown = ">=3.3,<4.0"
mkdocs = ">=1.1,<2.0"
[[package]]
name = "mkdocs-material"
version = "7.1.10"
description = "A Material Design theme for MkDocs"
category = "dev"
optional = false
python-versions = "*"
[package.dependencies]
markdown = ">=3.2"
mkdocs = ">=1.1"
mkdocs-material-extensions = ">=1.0"
Pygments = ">=2.4"
pymdown-extensions = ">=7.0"
[[package]]
name = "mkdocs-material-extensions"
version = "1.0.1"
description = "Extension pack for Python Markdown."
category = "dev"
optional = false
python-versions = ">=3.5"
[package.dependencies]
mkdocs-material = ">=5.0.0"
[[package]]
name = "mkdocstrings"
version = "0.15.2"
description = "Automatic documentation from sources, for MkDocs."
category = "dev"
optional = false
python-versions = ">=3.6,<4.0"
[package.dependencies]
Jinja2 = ">=2.11.1,<4.0"
Markdown = ">=3.3,<4.0"
MarkupSafe = ">=1.1,<3.0"
mkdocs = ">=1.1.1,<2.0.0"
mkdocs-autorefs = ">=0.1,<0.3"
pymdown-extensions = ">=6.3,<9.0"
pytkdocs = ">=0.2.0,<0.12.0"
[[package]]
name = "mypy"
version = "0.910"
@@ -192,6 +347,17 @@ category = "main"
optional = false
python-versions = ">=3.5"
[[package]]
name = "pymdown-extensions"
version = "8.2"
description = "Extension pack for Python Markdown."
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
Markdown = ">=3.2"
[[package]]
name = "pyparsing"
version = "2.4.7"
@@ -238,6 +404,52 @@ toml = "*"
[package.extras]
testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"]
[[package]]
name = "python-dateutil"
version = "2.8.2"
description = "Extensions to the standard Python datetime module"
category = "dev"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
[package.dependencies]
six = ">=1.5"
[[package]]
name = "pytkdocs"
version = "0.11.1"
description = "Load Python objects documentation."
category = "dev"
optional = false
python-versions = ">=3.6.1,<4.0.0"
[package.dependencies]
astunparse = {version = ">=1.6.3,<2.0.0", markers = "python_version < \"3.9\""}
cached-property = {version = ">=1.5.2,<2.0.0", markers = "python_version < \"3.8\""}
typing-extensions = {version = ">=3.7.4.3,<4.0.0.0", markers = "python_version < \"3.8\""}
[package.extras]
numpy-style = ["docstring_parser (>=0.7.3,<0.8.0)"]
[[package]]
name = "pyyaml"
version = "5.4.1"
description = "YAML parser and emitter for Python"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
[[package]]
name = "pyyaml-env-tag"
version = "0.1"
description = "A custom YAML tag for referencing environment variables in YAML files. "
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
pyyaml = "*"
[[package]]
name = "regex"
version = "2021.7.6"
@@ -263,6 +475,14 @@ typing-extensions = {version = ">=3.7.4,<4.0.0", markers = "python_version < \"3
[package.extras]
jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"]
[[package]]
name = "six"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "toml"
version = "0.10.2"
@@ -287,6 +507,17 @@ category = "main"
optional = false
python-versions = "*"
[[package]]
name = "watchdog"
version = "2.1.3"
description = "Filesystem events monitoring"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.extras]
watchmedo = ["PyYAML (>=3.10)", "argh (>=0.24.1)"]
[[package]]
name = "zipp"
version = "3.5.0"
@@ -302,13 +533,17 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes
[metadata]
lock-version = "1.1"
python-versions = "^3.7"
content-hash = "137e746aef38f8e8eac2674219503221c2705c80b9ee4b463129b1c47e459460"
content-hash = "ac64f9d88e11df57c6fabe30fb9f39aee4ea9925809a8c850a61e0b4e92bf64f"
[metadata.files]
appdirs = [
{file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
{file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
]
astunparse = [
{file = "astunparse-1.6.3-py2.py3-none-any.whl", hash = "sha256:c2652417f2c8b5bb325c885ae329bdf3f86424075c4fd1a128674bc6fba4b8e8"},
{file = "astunparse-1.6.3.tar.gz", hash = "sha256:5ad93a8456f0d084c3456d059fd9a92cce667963232cbf763eac3bc5b7940872"},
]
atomicwrites = [
{file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
{file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
@@ -320,6 +555,10 @@ attrs = [
black = [
{file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"},
]
cached-property = [
{file = "cached-property-1.5.2.tar.gz", hash = "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130"},
{file = "cached_property-1.5.2-py2.py3-none-any.whl", hash = "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0"},
]
click = [
{file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"},
{file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"},
@@ -386,6 +625,9 @@ coverage = [
{file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"},
{file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"},
]
ghp-import = [
{file = "ghp-import-2.0.1.tar.gz", hash = "sha256:753de2eace6e0f7d4edfb3cce5e3c3b98cd52aadb80163303d1d036bda7b4483"},
]
importlib-metadata = [
{file = "importlib_metadata-4.6.1-py3-none-any.whl", hash = "sha256:9f55f560e116f8643ecf2922d9cd3e1c7e8d52e683178fecd9d08f6aa357e11e"},
{file = "importlib_metadata-4.6.1.tar.gz", hash = "sha256:079ada16b7fc30dfbb5d13399a5113110dab1aa7c2bc62f66af75f0b717c8cac"},
@@ -394,6 +636,74 @@ iniconfig = [
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
]
jinja2 = [
{file = "Jinja2-3.0.1-py3-none-any.whl", hash = "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4"},
{file = "Jinja2-3.0.1.tar.gz", hash = "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4"},
]
markdown = [
{file = "Markdown-3.3.4-py3-none-any.whl", hash = "sha256:96c3ba1261de2f7547b46a00ea8463832c921d3f9d6aba3f255a6f71386db20c"},
{file = "Markdown-3.3.4.tar.gz", hash = "sha256:31b5b491868dcc87d6c24b7e3d19a0d730d59d3e46f4eea6430a321bed387a49"},
]
markupsafe = [
{file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"},
{file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"},
{file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"},
{file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"},
{file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"},
{file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"},
{file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"},
{file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"},
{file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"},
]
mergedeep = [
{file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"},
{file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"},
]
mkdocs = [
{file = "mkdocs-1.2.1-py3-none-any.whl", hash = "sha256:11141126e5896dd9d279b3e4814eb488e409a0990fb638856255020406a8e2e7"},
{file = "mkdocs-1.2.1.tar.gz", hash = "sha256:6e0ea175366e3a50d334597b0bc042b8cebd512398cdd3f6f34842d0ef524905"},
]
mkdocs-autorefs = [
{file = "mkdocs-autorefs-0.2.1.tar.gz", hash = "sha256:b8156d653ed91356e71675ce1fa1186d2b2c2085050012522895c9aa98fca3e5"},
{file = "mkdocs_autorefs-0.2.1-py3-none-any.whl", hash = "sha256:f301b983a34259df90b3fcf7edc234b5e6c7065bd578781e66fd90b8cfbe76be"},
]
mkdocs-material = [
{file = "mkdocs-material-7.1.10.tar.gz", hash = "sha256:890e9be00bfbe4d22ccccbcde1bf9bad67a3ba495f2a7d2422ea4acb5099f014"},
{file = "mkdocs_material-7.1.10-py2.py3-none-any.whl", hash = "sha256:92ff8c4a8e78555ef7b7ed0ba3043421d18971b48d066ea2cefb50e889fc66db"},
]
mkdocs-material-extensions = [
{file = "mkdocs-material-extensions-1.0.1.tar.gz", hash = "sha256:6947fb7f5e4291e3c61405bad3539d81e0b3cd62ae0d66ced018128af509c68f"},
{file = "mkdocs_material_extensions-1.0.1-py3-none-any.whl", hash = "sha256:d90c807a88348aa6d1805657ec5c0b2d8d609c110e62b9dce4daf7fa981fa338"},
]
mkdocstrings = [
{file = "mkdocstrings-0.15.2-py3-none-any.whl", hash = "sha256:8d6cbe64c07ae66739010979ca01d49dd2f64d1a45009f089d217b9cd2a65e36"},
{file = "mkdocstrings-0.15.2.tar.gz", hash = "sha256:c2fee9a3a644647c06eb2044fdfede1073adfd1a55bf6752005d3db10705fe73"},
]
mypy = [
{file = "mypy-0.910-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:a155d80ea6cee511a3694b108c4494a39f42de11ee4e61e72bc424c490e46457"},
{file = "mypy-0.910-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b94e4b785e304a04ea0828759172a15add27088520dc7e49ceade7834275bedb"},
@@ -443,6 +753,10 @@ pygments = [
{file = "Pygments-2.9.0-py3-none-any.whl", hash = "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e"},
{file = "Pygments-2.9.0.tar.gz", hash = "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f"},
]
pymdown-extensions = [
{file = "pymdown-extensions-8.2.tar.gz", hash = "sha256:b6daa94aad9e1310f9c64c8b1f01e4ce82937ab7eb53bfc92876a97aca02a6f4"},
{file = "pymdown_extensions-8.2-py3-none-any.whl", hash = "sha256:141452d8ed61165518f2c923454bf054866b85cf466feedb0eb68f04acdc2560"},
]
pyparsing = [
{file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
{file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
@@ -455,6 +769,49 @@ pytest-cov = [
{file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"},
{file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"},
]
python-dateutil = [
{file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
{file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
]
pytkdocs = [
{file = "pytkdocs-0.11.1-py3-none-any.whl", hash = "sha256:89ca4926d0acc266235beb24cb0b0591aa6bf7adedfae54bf9421d529d782c8d"},
{file = "pytkdocs-0.11.1.tar.gz", hash = "sha256:1ec7e028fe8361acc1ce909ada4e6beabec28ef31e629618549109e1d58549f0"},
]
pyyaml = [
{file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"},
{file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"},
{file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"},
{file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"},
{file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"},
{file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"},
{file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347"},
{file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541"},
{file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"},
{file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"},
{file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"},
{file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"},
{file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa"},
{file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"},
{file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"},
{file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"},
{file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"},
{file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"},
{file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247"},
{file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc"},
{file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"},
{file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"},
{file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"},
{file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"},
{file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122"},
{file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6"},
{file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"},
{file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"},
{file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"},
]
pyyaml-env-tag = [
{file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"},
{file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"},
]
regex = [
{file = "regex-2021.7.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e6a1e5ca97d411a461041d057348e578dc344ecd2add3555aedba3b408c9f874"},
{file = "regex-2021.7.6-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:6afe6a627888c9a6cfbb603d1d017ce204cebd589d66e0703309b8048c3b0854"},
@@ -502,6 +859,10 @@ rich = [
{file = "rich-10.6.0-py3-none-any.whl", hash = "sha256:d3f72827cd5df13b2ef7f1a97f81ec65548d4fdeb92cef653234f227580bbb2a"},
{file = "rich-10.6.0.tar.gz", hash = "sha256:128261b3e2419a4ef9c97066ccc2abbfb49fa7c5e89c3fe4056d00aa5e9c1e65"},
]
six = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
toml = [
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
@@ -543,6 +904,29 @@ typing-extensions = [
{file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"},
{file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"},
]
watchdog = [
{file = "watchdog-2.1.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9628f3f85375a17614a2ab5eac7665f7f7be8b6b0a2a228e6f6a2e91dd4bfe26"},
{file = "watchdog-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:acc4e2d5be6f140f02ee8590e51c002829e2c33ee199036fcd61311d558d89f4"},
{file = "watchdog-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:85b851237cf3533fabbc034ffcd84d0fa52014b3121454e5f8b86974b531560c"},
{file = "watchdog-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a12539ecf2478a94e4ba4d13476bb2c7a2e0a2080af2bb37df84d88b1b01358a"},
{file = "watchdog-2.1.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6fe9c8533e955c6589cfea6f3f0a1a95fb16867a211125236c82e1815932b5d7"},
{file = "watchdog-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d9456f0433845e7153b102fffeb767bde2406b76042f2216838af3b21707894e"},
{file = "watchdog-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fd8c595d5a93abd441ee7c5bb3ff0d7170e79031520d113d6f401d0cf49d7c8f"},
{file = "watchdog-2.1.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0bcfe904c7d404eb6905f7106c54873503b442e8e918cc226e1828f498bdc0ca"},
{file = "watchdog-2.1.3-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bf84bd94cbaad8f6b9cbaeef43080920f4cb0e61ad90af7106b3de402f5fe127"},
{file = "watchdog-2.1.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b8ddb2c9f92e0c686ea77341dcb58216fa5ff7d5f992c7278ee8a392a06e86bb"},
{file = "watchdog-2.1.3-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8805a5f468862daf1e4f4447b0ccf3acaff626eaa57fbb46d7960d1cf09f2e6d"},
{file = "watchdog-2.1.3-py3-none-manylinux2014_armv7l.whl", hash = "sha256:3e305ea2757f81d8ebd8559d1a944ed83e3ab1bdf68bcf16ec851b97c08dc035"},
{file = "watchdog-2.1.3-py3-none-manylinux2014_i686.whl", hash = "sha256:431a3ea70b20962e6dee65f0eeecd768cd3085ea613ccb9b53c8969de9f6ebd2"},
{file = "watchdog-2.1.3-py3-none-manylinux2014_ppc64.whl", hash = "sha256:e4929ac2aaa2e4f1a30a36751160be391911da463a8799460340901517298b13"},
{file = "watchdog-2.1.3-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:201cadf0b8c11922f54ec97482f95b2aafca429c4c3a4bb869a14f3c20c32686"},
{file = "watchdog-2.1.3-py3-none-manylinux2014_s390x.whl", hash = "sha256:3a7d242a7963174684206093846537220ee37ba9986b824a326a8bb4ef329a33"},
{file = "watchdog-2.1.3-py3-none-manylinux2014_x86_64.whl", hash = "sha256:54e057727dd18bd01a3060dbf5104eb5a495ca26316487e0f32a394fd5fe725a"},
{file = "watchdog-2.1.3-py3-none-win32.whl", hash = "sha256:b5fc5c127bad6983eecf1ad117ab3418949f18af9c8758bd10158be3647298a9"},
{file = "watchdog-2.1.3-py3-none-win_amd64.whl", hash = "sha256:44acad6f642996a2b50bb9ce4fb3730dde08f23e79e20cd3d8e2a2076b730381"},
{file = "watchdog-2.1.3-py3-none-win_ia64.whl", hash = "sha256:0bcdf7b99b56a3ae069866c33d247c9994ffde91b620eaf0306b27e099bd1ae0"},
{file = "watchdog-2.1.3.tar.gz", hash = "sha256:e5236a8e8602ab6db4b873664c2d356c365ab3cac96fbdec4970ad616415dd45"},
]
zipp = [
{file = "zipp-3.5.0-py3-none-any.whl", hash = "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3"},
{file = "zipp-3.5.0.tar.gz", hash = "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4"},

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "textual"
version = "0.1.6"
version = "0.1.7"
homepage = "https://github.com/willmcgugan/textual"
description = "Text User Interface using Rich"
authors = ["Will McGugan <willmcgugan@gmail.com>"]
@@ -31,6 +31,9 @@ pytest = "^6.2.3"
black = "^20.8b1"
mypy = "^0.910"
pytest-cov = "^2.12.1"
mkdocs = "^1.2.1"
mkdocstrings = "^0.15.2"
mkdocs-material = "^7.1.10"
[build-system]
requires = ["poetry-core>=1.0.0"]

View File

@@ -23,7 +23,7 @@ from .binding import Bindings, NoBinding
from .geometry import Point, Region
from ._context import active_app
from ._event_broker import extract_handler_actions, NoHandler
from .keys import Binding
from ._types import MessageTarget
from .driver import Driver
from .layouts.dock import DockLayout, Dock
from ._linux_driver import LinuxDriver
@@ -56,6 +56,12 @@ ViewType = TypeVar("ViewType", bound=View)
# uvloop.install()
class PanicMessage(Message):
def __init__(self, sender: MessageTarget, traceback: Traceback) -> None:
self.traceback = traceback
super().__init__(sender)
class ActionError(Exception):
pass
@@ -71,11 +77,19 @@ class App(MessagePump):
def __init__(
self,
console: Console = None,
console: Console | None = None,
screen: bool = True,
driver_class: Type[Driver] = None,
driver_class: Type[Driver] | None = None,
title: str = "Textual Application",
):
"""The Textual Application base class
Args:
console (Console, optional): A Rich Console. Defaults to None.
screen (bool, optional): Enable full-screen application mode. Defaults to True.
driver_class (Type[Driver], optional): Driver class, or None to use default. Defaults to None.
title (str, optional): Title of the application. Defaults to "Textual Application".
"""
self.console = console or get_console()
self._screen = screen
self.driver_class = driver_class or LinuxDriver
@@ -88,6 +102,7 @@ class App(MessagePump):
self.mouse_over: Widget | None = None
self.mouse_captured: Widget | None = None
self._driver: Driver | None = None
self._tracebacks: list[Traceback] = []
self._docks: list[Dock] = []
self._action_targets = {"app", "view"}
@@ -202,10 +217,12 @@ class App(MessagePump):
if widget is not None:
await widget.post_message(events.MouseCaptured(self, self.mouse_position))
def panic(self, traceback: Traceback) -> None:
self._tracebacks.append(traceback)
self.close_messages_no_wait()
async def process_messages(self) -> None:
log.debug("driver=%r", self.driver_class)
# loop = asyncio.get_event_loop()
# loop.add_signal_handler(signal.SIGINT, self.on_keyboard_interupt)
driver = self._driver = self.driver_class(self.console, self)
active_app.set(self)
@@ -242,10 +259,10 @@ class App(MessagePump):
view = self._view_stack.pop()
await view.close_messages()
except Exception:
traceback = Traceback(show_locals=True)
self._tracebacks.append(Traceback(show_locals=True))
finally:
driver.stop_application_mode()
if traceback is not None:
for traceback in self._tracebacks:
self.console.print(traceback)
def require_repaint(self) -> None:
@@ -289,7 +306,7 @@ class App(MessagePump):
if sync_available:
console.file.write("\x1bP=2s\x1b\\")
except Exception:
log.exception("refresh failed")
self.panic(Traceback(show_locals=True))
def display(self, renderable: RenderableType) -> None:
if not self._closed:

View File

@@ -79,6 +79,16 @@ class Layout(ABC):
self.height = 0
self.renders: dict[Widget, tuple[Region, Lines]] = {}
self._cuts: list[list[int]] | None = None
self._require_update: bool = True
def check_update(self) -> bool:
return self._require_update
def require_update(self) -> None:
self._require_update = True
def reset_update(self) -> None:
self._require_update = False
def reset(self) -> None:
self._cuts = None
@@ -265,10 +275,12 @@ class Layout(ABC):
) -> Iterable[list[Segment]]:
for bucket in chops:
yield sum(
(segments for _, segments in sorted(bucket.items()) if segments),
start=[],
)
line: list[Segment] = []
extend = line.extend
for _, segments in sorted(bucket.items()):
if segments:
extend(segments)
yield line
def render(
self,

View File

@@ -9,7 +9,7 @@ import sys
from typing import Iterable, NamedTuple
from .._layout_resolve import layout_resolve
from .._loop import loop_last
from ..geometry import Dimensions, Point, Region
from ..layout import Layout, OrderedRegion
from ..view import View
@@ -80,17 +80,39 @@ class GridLayout(Layout):
super().__init__()
def hide_row(self, row_name: str) -> None:
self.hidden_rows.add(row_name)
def is_row_visible(self, row_name: str) -> bool:
return row_name not in self.hidden_rows
def show_row(self, row_name: str) -> None:
self.hidden_rows.discard(row_name)
def is_column_visible(self, column_name: str) -> bool:
return column_name not in self.hidden_columns
def hide_column(self, column_name: str) -> None:
self.hidden_rows.add(column_name)
def show_row(self, row_name: str, visible: bool = True) -> bool:
changed = False
if visible:
if not self.is_row_visible(row_name):
self.require_update()
changed = True
self.hidden_rows.discard(row_name)
else:
if self.is_row_visible(row_name):
self.require_update()
changed = True
self.hidden_rows.add(row_name)
return changed
def show_column(self, column_name: str) -> None:
self.hidden_rows.discard(column_name)
def show_column(self, column_name: str, visible: bool = True) -> bool:
changed = False
if visible:
if not self.is_column_visible(column_name):
self.require_update()
changed = True
self.hidden_rows.discard(column_name)
else:
if self.is_column_visible(column_name):
self.require_update()
changed = True
self.hidden_rows.add(column_name)
return changed
def add_column(
self,

View File

@@ -27,7 +27,7 @@ class Message:
self.name = camel_to_snake(self.__class__.__name__.replace("Message", ""))
self.time = monotonic()
self._no_default_action = False
self._stop_propagaton = False
self._stop_propagation = False
super().__init__()
def __rich_repr__(self) -> rich.repr.RichReprResult:
@@ -57,5 +57,5 @@ class Message:
"""
self._no_default_action = prevent
def stop_propagation(self, stop: bool = True) -> None:
self._stop_propagaton = stop
def stop(self, stop: bool = True) -> None:
self._stop_propagation = stop

View File

@@ -1,19 +1,28 @@
from __future__ import annotations
import asyncio
from asyncio import CancelledError
from functools import partial
import logging
from asyncio import Event, Queue, QueueEmpty, Task
from typing import Any, Awaitable, Coroutine, NamedTuple
from asyncio import Queue, QueueEmpty, Task
from typing import TYPE_CHECKING, Awaitable, Iterable, Callable
from weakref import WeakSet
from rich.traceback import Traceback
from . import events
from ._timer import Timer, TimerCallback
from ._types import MessageHandler
from ._context import active_app
from .message import Message
log = logging.getLogger("rich")
if TYPE_CHECKING:
from .app import App
class NoParent(Exception):
pass
@@ -44,6 +53,15 @@ class MessagePump:
raise NoParent(f"{self._parent} has no parent")
return self._parent
@property
def app(self) -> "App":
"""Get the current app."""
return active_app.get()
@property
def is_parent_active(self):
return self._parent and not self._parent._closed and not self._parent._closing
def set_parent(self, parent: MessagePump) -> None:
self._parent = parent
@@ -120,6 +138,9 @@ class MessagePump:
self._child_tasks.add(asyncio.get_event_loop().create_task(timer.run()))
return timer
def close_messages_no_wait(self) -> None:
self._message_queue.put_nowait(None)
async def close_messages(self, wait: bool = True) -> None:
"""Close message queue, and optionally wait for queue to finish processing."""
if self._closed:
@@ -138,12 +159,20 @@ class MessagePump:
self._task = asyncio.create_task(self.process_messages())
async def process_messages(self) -> None:
try:
return await self._process_messages()
except CancelledError:
pass
async def _process_messages(self) -> None:
"""Process messages until the queue is closed."""
while not self._closed:
try:
message = await self.get_message()
except MessagePumpClosed:
break
except CancelledError:
raise
except Exception as error:
log.exception("error in get_message()")
raise error from None
@@ -161,9 +190,11 @@ class MessagePump:
try:
await self.dispatch_message(message)
except Exception as error:
log.exception("error in dispatch_message")
except CancelledError:
raise
except Exception as error:
self.app.panic(Traceback(show_locals=True))
break
finally:
if isinstance(message, events.Event) and self._message_queue.empty():
if not self._closed:
@@ -182,25 +213,39 @@ class MessagePump:
return await self.on_message(message)
return False
async def on_event(self, event: events.Event) -> None:
method_name = f"on_{event.name}"
def _get_dispatch_methods(
self, method_name: str, message: Message
) -> Iterable[Callable[[Message], Awaitable]]:
for cls in self.__class__.__mro__:
if message._no_default_action:
break
method = getattr(cls, method_name, None)
if method is not None:
yield method.__get__(self, cls)
dispatch_function: MessageHandler = getattr(self, method_name, None)
if dispatch_function is not None:
await dispatch_function(event)
if event.bubble and self._parent and not event._stop_propagaton:
if event.sender == self._parent:
pass
# log.debug("bubbled event abandoned; %r", event)
elif not self._parent._closed and not self._parent._closing:
async def on_event(self, event: events.Event) -> None:
_rich_traceback_guard = True
for method in self._get_dispatch_methods(f"on_{event.name}", event):
await method(event)
if event.bubble and self._parent and not event._stop_propagation:
if event.sender != self._parent and self.is_parent_active:
await self._parent.post_message(event)
async def on_message(self, message: Message) -> None:
_rich_traceback_guard = True
method_name = f"message_{message.name}"
method = getattr(self, method_name, None)
if method is not None:
for method in self._get_dispatch_methods(method_name, message):
await method(message)
if message.bubble and self._parent and not message._stop_propagation:
if message.sender == self._parent:
pass
elif not self._parent._closed and not self._parent._closing:
await self._parent.post_message(message)
def post_message_no_wait(self, message: Message) -> bool:
if self._closing or self._closed:
return False

View File

@@ -7,7 +7,6 @@ from rich.padding import Padding, PaddingDimensions
from rich.segment import Segment
from rich.style import StyleType
from . import events
from .geometry import Dimensions, Point
from .message import Message
from .widget import Widget, Reactive
@@ -56,7 +55,6 @@ class PageRender:
def render(self, console: Console, options: ConsoleOptions) -> None:
width = self.width or options.max_width or console.width
width *= 2
options = options.update_dimensions(width, None)
style = console.get_style(self.style)
renderable = self.renderable

View File

@@ -152,7 +152,6 @@ class ScrollBarRender:
def __rich_console__(
self, console: Console, options: ConsoleOptions
) -> RenderResult:
log.debug("SCROLLBAR RENDER")
size = (
(options.height or console.height)
if self.vertical
@@ -231,7 +230,6 @@ class ScrollBar(Widget):
async def on_mouse_up(self, event: events.MouseUp) -> None:
if self.grabbed:
await self.release_mouse()
await super().on_mouse_up(event)
async def on_mouse_captured(self, event: events.MouseCaptured) -> None:
self.grabbed = event.mouse_position

View File

@@ -44,6 +44,9 @@ class View(Widget):
def __rich_repr__(self) -> rich.repr.RichReprResult:
yield "name", self.name
def __getitem__(self, widget_name: str) -> Widget:
return self.named_widgets[widget_name]
@property
def is_visual(self) -> bool:
return False

View File

@@ -87,11 +87,6 @@ class Widget(MessagePump):
def is_visual(self) -> bool:
return True
@property
def app(self) -> "App":
"""Get the current app."""
return active_app.get()
@property
def console(self) -> Console:
"""Get the current console."""

View File

@@ -7,10 +7,16 @@ from rich.panel import Panel
import rich.repr
from rich.style import StyleType
from .. import events
from ..message import Message
from ..reactive import Reactive
from ..widget import Widget
class ButtonPressed(Message, bubble=True):
pass
class Expand:
def __init__(self, renderable: RenderableType) -> None:
self.renderable = renderable
@@ -48,11 +54,15 @@ class Button(Widget):
name: str | None = None,
style: StyleType = "white on dark_blue",
):
self.label = label
self.name = name or str(label)
self.style = style
super().__init__()
super().__init__(name=name)
self.label = label
label: Reactive[RenderableType] = Reactive("")
def render(self) -> RenderableType:
return ButtonRenderable(self.label, style=self.style)
return Align.center(self.label, vertical="middle", style=self.style)
async def on_click(self, event: events.Click) -> None:
await self.emit(ButtonPressed(self))

View File

@@ -29,18 +29,19 @@ class ScrollView(View):
fluid: bool = True,
) -> None:
self.fluid = fluid
self._vertical_scrollbar = ScrollBar(vertical=True)
self._horizontal_scrollbar = ScrollBar(vertical=False)
self._page = Page(renderable or "", style=style)
self.vscroll = ScrollBar(vertical=True)
self.hscroll = ScrollBar(vertical=False)
self.page = Page(renderable or "", style=style)
layout = GridLayout()
layout.add_column("main")
layout.add_column("vertical", size=1)
layout.add_column("vscroll", size=1)
layout.add_row("main")
layout.add_row("horizontal", size=1)
layout.add_row("hscroll", size=1)
layout.add_areas(
content="main,main", vertical="vertical,main", horizontal="main,horizontal"
content="main,main", vscroll="vscroll,main", hscroll="main,hscroll"
)
# layout.hide_row("horizontal")
layout.show_row("hscroll", False)
layout.show_row("vscroll", False)
super().__init__(name=name, layout=layout)
x: Reactive[float] = Reactive(0)
@@ -50,35 +51,35 @@ class ScrollView(View):
target_y: Reactive[float] = Reactive(0)
def validate_x(self, value: float) -> float:
return clamp(value, 0, self._page.contents_size.width - self.size.width)
return clamp(value, 0, self.page.contents_size.width - self.size.width)
def validate_target_x(self, value: float) -> float:
return clamp(value, 0, self._page.contents_size.width - self.size.width)
return clamp(value, 0, self.page.contents_size.width - self.size.width)
def validate_y(self, value: float) -> float:
return clamp(value, 0, self._page.contents_size.height - self.size.height)
return clamp(value, 0, self.page.contents_size.height - self.size.height)
def validate_target_y(self, value: float) -> float:
return clamp(value, 0, self._page.contents_size.height - self.size.height)
return clamp(value, 0, self.page.contents_size.height - self.size.height)
async def watch_x(self, new_value: float) -> None:
self._page.x = round(new_value)
self._horizontal_scrollbar.position = round(new_value)
self.page.x = round(new_value)
self.hscroll.position = round(new_value)
async def watch_y(self, new_value: float) -> None:
self._page.y = round(new_value)
self._vertical_scrollbar.position = round(new_value)
self.page.y = round(new_value)
self.vscroll.position = round(new_value)
async def update(self, renderabe: RenderableType) -> None:
self._page.update(renderabe)
self.page.update(renderabe)
self.require_repaint()
async def on_mount(self, event: events.Mount) -> None:
assert isinstance(self.layout, GridLayout)
self.layout.place(
content=self._page,
vertical=self._vertical_scrollbar,
horizontal=self._horizontal_scrollbar,
content=self.page,
vscroll=self.vscroll,
hscroll=self.hscroll,
)
await self.layout.mount_all(self)
@@ -133,7 +134,7 @@ class ScrollView(View):
async def key_end(self) -> None:
self.target_x = 0
self.target_y = self._page.contents_size.height - self.size.height
self.target_y = self.page.contents_size.height - self.size.height
self.animate("x", self.target_x, duration=1, easing="out_cubic")
self.animate("y", self.target_y, duration=1, easing="out_cubic")
@@ -146,7 +147,7 @@ class ScrollView(View):
async def on_resize(self, event: events.Resize) -> None:
await super().on_resize(event)
if self.fluid:
self._page.update()
self.page.update()
async def message_scroll_up(self, message: Message) -> None:
self.page_up()
@@ -171,7 +172,17 @@ class ScrollView(View):
async def message_page_update(self, message: Message) -> None:
self.x = self.validate_x(self.x)
self.y = self.validate_y(self.y)
self._horizontal_scrollbar.virtual_size = self._page.virtual_size.width
self._horizontal_scrollbar.window_size = self.size.width
self._vertical_scrollbar.virtual_size = self._page.virtual_size.height
self._vertical_scrollbar.window_size = self.size.height
self.vscroll.virtual_size = self.page.virtual_size.height
self.vscroll.window_size = self.size.height
if self.layout.show_column(
"vscroll", self.page.virtual_size.height > self.size.height
):
self.page.update()
self.hscroll.virtual_size = self.page.virtual_size.width
self.hscroll.window_size = self.size.width
if self.layout.show_row(
"hscroll", self.page.virtual_size.width > self.size.width
):
self.page.update()