mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
file monitor
This commit is contained in:
@@ -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
6
poetry.lock
generated
@@ -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"},
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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__":
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
31
src/textual/file_monitor.py
Normal file
31
src/textual/file_monitor.py
Normal 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)
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user