file monitor

This commit is contained in:
Will McGugan
2021-12-18 13:17:48 +00:00
parent 485c1bdfe1
commit 72f933b844
10 changed files with 120 additions and 29 deletions

View File

@@ -6,7 +6,7 @@ class BasicApp(App):
"""A basic app demonstrating CSS"""
def on_load(self):
self.bind("t", "toggle('#sidebar', '-active')")
self.bind("t", "toggle_class('#sidebar', '-active')")
def on_mount(self):
"""Build layout here."""
@@ -18,4 +18,4 @@ class BasicApp(App):
)
BasicApp.run(log="textual.log", css_file="basic.css", log_verbosity=3)
BasicApp.run(log="textual.log", css_file="basic.css", watch_css=True)

6
poetry.lock generated
View File

@@ -547,7 +547,7 @@ python-versions = "*"
[[package]]
name = "rich"
version = "10.15.2"
version = "10.16.1"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
category = "main"
optional = false
@@ -995,8 +995,8 @@ regex = [
{file = "regex-2021.8.21.tar.gz", hash = "sha256:faf08b0341828f6a29b8f7dd94d5cf8cc7c39bfc3e67b78514c54b494b66915a"},
]
rich = [
{file = "rich-10.15.2-py3-none-any.whl", hash = "sha256:43b2c6ad51f46f6c94992aee546f1c177719f4e05aff8f5ea4d2efae3ebdac89"},
{file = "rich-10.15.2.tar.gz", hash = "sha256:1dded089b79dd042b3ab5cd63439a338e16652001f0c16e73acdcf4997ad772d"},
{file = "rich-10.16.1-py3-none-any.whl", hash = "sha256:bbe04dd6ac09e4b00d22cb1051aa127beaf6e16c3d8687b026e96d3fca6aad52"},
{file = "rich-10.16.1.tar.gz", hash = "sha256:4949e73de321784ef6664ebbc854ac82b20ff60b2865097b93f3b9b41e30da27"},
]
six = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},

View File

@@ -25,9 +25,11 @@ from .geometry import Offset, Region
from . import log
from ._callback import invoke
from ._context import active_app
from .css.stylesheet import Stylesheet, StylesheetParseError
from .css.stylesheet import Stylesheet, StylesheetParseError, StylesheetError
from ._event_broker import extract_handler_actions, NoHandler
from .driver import Driver
from .file_monitor import FileMonitor
from .layouts.dock import DockLayout, Dock
from ._linux_driver import LinuxDriver
from ._types import MessageTarget
@@ -79,6 +81,7 @@ class App(DOMNode):
title: str = "Textual Application",
css_file: str | None = None,
css: str | None = None,
watch_css: bool = True,
):
"""The Textual Application base class
@@ -119,6 +122,11 @@ class App(DOMNode):
self.stylesheet = Stylesheet()
self.css_file = css_file
self.css_monitor = (
FileMonitor(css_file, self._on_css_change)
if (watch_css and css_file)
else None
)
if css is not None:
self.css = css
@@ -148,9 +156,6 @@ class App(DOMNode):
def css_type(self) -> str:
return "app"
def load_css(self, filename: str) -> None:
pass
def log(self, *args: Any, verbosity: int = 1, **kwargs) -> None:
"""Write to logs.
@@ -214,6 +219,24 @@ class App(DOMNode):
asyncio.run(run_app())
async def _on_css_change(self) -> None:
self.log("CSS changed")
self.log("css_file", self.css_file)
if self.css_file is not None:
stylesheet = Stylesheet()
try:
self.log("loading", self.css_file)
stylesheet.read(self.css_file)
except StylesheetError as error:
self.log(error)
self.console.bell()
else:
self.log("reseting stylesheet")
self.reset_styles()
self.stylesheet = stylesheet
self.stylesheet.update(self)
self.view.refresh(layout=True)
def mount(self, *anon_widgets: Widget, **widgets: Widget) -> None:
self.register(self.view, *anon_widgets, **widgets)
self.view.refresh()
@@ -317,6 +340,10 @@ class App(DOMNode):
self._print_error_renderables()
return
if self.css_monitor:
self.set_interval(0.5, self.css_monitor)
self.log("started", self.css_monitor)
self._running = True
try:
load_event = events.Load(sender=self)
@@ -548,6 +575,15 @@ class App(DOMNode):
return False
return True
async def handle_update(self, message: messages.Update) -> None:
message.stop()
self.app.refresh()
async def handle_layout(self, message: messages.Layout) -> None:
message.stop()
await self.view.refresh_layout()
self.app.refresh()
async def on_key(self, event: events.Key) -> None:
await self.press(event.key)
@@ -570,7 +606,15 @@ class App(DOMNode):
async def action_bell(self) -> None:
self.console.bell()
async def action_toggle(self, selector: str, class_name: str) -> None:
async def action_add_class_(self, selector: str, class_name: str) -> None:
self.view.query(selector).add_class(class_name)
self.view.refresh(layout=True)
async def action_remove_class_(self, selector: str, class_name: str) -> None:
self.view.query(selector).remove_class(class_name)
self.view.refresh(layout=True)
async def action_toggle_class(self, selector: str, class_name: str) -> None:
self.view.query(selector).toggle_class(class_name)
self.view.refresh(layout=True)

View File

@@ -132,6 +132,7 @@ class RuleSet:
selector_set: list[SelectorSet] = field(default_factory=list)
styles: Styles = field(default_factory=Styles)
errors: list[tuple[Token, str]] = field(default_factory=list)
classes: set[str] = field(default_factory=set)
@classmethod
def _selector_to_css(cls, selectors: list[Selector]) -> str:
@@ -161,3 +162,15 @@ class RuleSet:
declarations = "\n".join(f" {line}" for line in self.styles.css_lines)
css = f"{self.selectors} {{\n{declarations}\n}}"
return css
def _post_parse(self) -> None:
"""Called after the RuleSet is parsed."""
# Build a set of the class names that have been updated
update = self.classes.update
class_type = SelectorType.CLASS
for selector_set in self.selector_set:
update(
selector.name
for selector in selector_set.selectors
if selector.type == class_type
)

View File

@@ -153,6 +153,7 @@ def parse_rule_set(tokens: Iterator[Token], token: Token) -> Iterable[RuleSet]:
rule_set = RuleSet(
list(SelectorSet.from_selectors(rule_selectors)), styles_builder.styles, errors
)
rule_set._post_parse()
yield rule_set

View File

@@ -83,8 +83,6 @@ class Styles:
_rule_min_width: Scalar | None = None
_rule_min_height: Scalar | None = None
_rule_layout: str | None = None
_rule_dock_group: str | None = None
_rule_docks: tuple[DockGroup, ...] | None = None

View File

@@ -9,9 +9,7 @@ from rich.console import RenderableType
import rich.repr
from rich.highlighter import ReprHighlighter
from rich.panel import Panel
from rich.syntax import Syntax
from rich.table import Table
from rich.text import TextType, Text
from rich.text import Text
from rich.console import Group, RenderableType
@@ -114,8 +112,9 @@ class Stylesheet:
rule_attributes: dict[str, list[tuple[Specificity4, object]]]
rule_attributes = defaultdict(list)
_check_rule = self._check_rule
for rule in self.rules:
for specificity in self._check_rule(rule, node):
for specificity in _check_rule(rule, node):
for key, rule_specificity, value in rule.styles.extract_rules(
specificity
):
@@ -123,8 +122,6 @@ class Stylesheet:
get_first_item = itemgetter(0)
log(rule_attributes.get("offset"))
node_rules = [
(name, max(specificity_rules, key=get_first_item)[1])
for name, specificity_rules in rule_attributes.items()
@@ -132,6 +129,12 @@ class Stylesheet:
node.styles.apply_rules(node_rules)
def update(self, root: DOMNode) -> None:
"""Update a node and its children."""
apply = self.apply
for node in root.walk_children():
apply(node)
if __name__ == "__main__":

View File

@@ -131,6 +131,14 @@ class DOMNode(MessagePump):
add_children(tree, self)
return tree
def reset_styles(self) -> None:
from .widget import Widget
for node in self.walk_children():
node.styles = Styles()
if isinstance(node, Widget):
node.clear_render_cache()
def add_child(self, node: DOMNode) -> None:
self.children._append(node)
node.set_parent(self)
@@ -172,7 +180,7 @@ class DOMNode(MessagePump):
def toggle_class(self, *class_names: str) -> None:
"""Toggle class names"""
self._classes.symmetric_difference_update(class_names)
self.app.stylesheet.apply(self)
self.app.stylesheet.update(self.app)
self.log(self.styles.css)
def has_psuedo_class(self, *class_names: str) -> bool:

View File

@@ -0,0 +1,31 @@
import os
from typing import Callable
import rich.repr
from ._callback import invoke
@rich.repr.auto
class FileMonitor:
def __init__(self, path: str, callback: Callable) -> None:
self.path = path
self.callback = callback
self._modified = self._get_modified()
def _get_modified(self) -> float:
return os.stat(self.path).st_mtime
def check(self) -> bool:
modified = self._get_modified()
changed = modified != self._modified
self._modified = modified
return changed
async def __call__(self) -> None:
if self.check():
await self.on_change()
async def on_change(self) -> None:
"""Called when file changes."""
await invoke(self.callback)

View File

@@ -84,7 +84,7 @@ class Widget(DOMNode):
# layout_offset_x: Reactive[float] = Reactive(0.0, layout=True)
# layout_offset_y: Reactive[float] = Reactive(0.0, layout=True)
style: Reactive[str | None] = Reactive(None)
# style: Reactive[str | None] = Reactive(None)
padding: Reactive[Spacing | None] = Reactive(None, layout=True)
margin: Reactive[Spacing | None] = Reactive(None, layout=True)
border: Reactive[str] = Reactive("none", layout=True)
@@ -97,12 +97,6 @@ class Widget(DOMNode):
def validate_margin(self, margin: SpacingDimensions) -> Spacing:
return Spacing.unpack(margin)
# def validate_layout_offset_x(self, value) -> int:
# return int(value)
# def validate_layout_offset_y(self, value) -> int:
# return int(value)
def __init_subclass__(cls, can_focus: bool = True) -> None:
super().__init_subclass__()
cls.can_focus = can_focus
@@ -285,18 +279,17 @@ class Widget(DOMNode):
self.refresh()
async def on_idle(self, event: events.Idle) -> None:
self.log("Widget.on_idle")
if self.check_layout():
self.log("layout required")
self.render_cache = None
self.reset_check_repaint()
self.reset_check_layout()
await self.post_message(Layout(self))
await self.emit(Layout(self))
elif self.check_repaint():
self.log("repaint required")
self.render_cache = None
self.reset_check_repaint()
await self.post_message(Update(self, self))
await self.emit(Update(self, self))
async def focus(self) -> None:
await self.app.set_focus(self)