Skip to content

Commit 8c63c6c

Browse files
committed
Actions and updates
1 parent 223636d commit 8c63c6c

File tree

6 files changed

+106
-19
lines changed

6 files changed

+106
-19
lines changed

src/textual/_line_cache.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ def from_renderable(
2323
height: int,
2424
) -> "LineCache":
2525
options = console.options.update_dimensions(width, height)
26-
lines = console.render_lines(renderable, options, new_lines=True)
26+
lines = console.render_lines(renderable, options, new_lines=False)
2727
return cls(lines)
2828

2929
@property
@@ -40,8 +40,10 @@ def __rich_console__(
4040

4141
def render(self, x: int, y: int) -> Iterable[Segment]:
4242
move_to = Control.move_to
43+
new_line = Segment.line()
4344
for offset_y, (line, dirty) in enumerate(zip(self.lines, self._dirty), y):
4445
if dirty:
4546
yield move_to(x, offset_y).segment
4647
yield from line
48+
yield new_line
4749
self._dirty[:] = [False] * len(self.lines)

src/textual/actions.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from __future__ import annotations
2+
3+
import ast
4+
from typing import Any, Tuple
5+
import re
6+
7+
8+
class ActionError(Exception):
9+
pass
10+
11+
12+
re_action_params = re.compile(r"([\w\.]+)(\(.*?\))")
13+
14+
15+
def parse(action: str) -> tuple[str, tuple[Any, ...]]:
16+
params_match = re_action_params.match(action)
17+
if params_match is not None:
18+
action_name, action_params_str = params_match.groups()
19+
try:
20+
action_params = ast.literal_eval(action_params_str)
21+
except Exception as error:
22+
raise ActionError(str(error))
23+
else:
24+
action_name = action
25+
action_params = ()
26+
27+
return (
28+
action_name,
29+
action_params if isinstance(action_params, tuple) else (action_params,),
30+
)
31+
32+
33+
if __name__ == "__main__":
34+
35+
print(parse("view.toggle('side')"))
36+
37+
print(parse("view.toggle"))

src/textual/app.py

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from rich.console import Console
1414

1515
from . import events
16+
from . import actions
1617
from ._context import active_app
1718
from .driver import Driver
1819
from ._linux_driver import LinuxDriver
@@ -22,7 +23,7 @@
2223
log = logging.getLogger("rich")
2324

2425

25-
LayoutDefinition = dict[str, Any]
26+
LayoutDefinition = "dict[str, Any]"
2627

2728
try:
2829
import uvloop
@@ -54,6 +55,8 @@ def __init__(
5455
self.view = view or LayoutView()
5556
self.children: set[MessagePump] = set()
5657

58+
self._action_targets = {"app": self, "view": self.view}
59+
5760
def __rich_repr__(self) -> RichReprResult:
5861
yield "title", self.title
5962

@@ -107,9 +110,7 @@ async def add(self, child: MessagePump) -> None:
107110
def refresh(self) -> None:
108111
console = self.console
109112
with console:
110-
console.print(
111-
Screen(Control.home(), self.view, Control.home(), application_mode=True)
112-
)
113+
console.print(Screen(Control.home(), self.view, Control.home()))
113114

114115
async def on_event(self, event: events.Event, priority: int) -> None:
115116
if isinstance(event, events.Key):
@@ -128,18 +129,38 @@ async def on_idle(self, event: events.Idle) -> None:
128129
await self.view.post_message(event)
129130

130131
async def action(self, action: str) -> None:
131-
if "." in action:
132-
destination, action_name, *tokens = action.split(".")
132+
"""Perform an action.
133+
134+
Args:
135+
action (str): Action encoded in a string.
136+
"""
137+
138+
target, params = actions.parse(action)
139+
if "." in target:
140+
destination, action_name = target.split(".", 1)
133141
else:
134142
destination = "app"
135143
action_name = action
136-
tokens = []
137144

138-
if destination == "app":
145+
log.debug("ACTION %r %r", destination, action_name)
146+
await self.dispatch_action(destination, action_name, params)
147+
148+
async def dispatch_action(
149+
self, destination: str, action_name: str, params: Any
150+
) -> None:
151+
action_target = self._action_targets.get(destination, None)
152+
log.debug("ACTION TARGET %r", action_target)
153+
if action_target is not None:
139154
method_name = f"action_{action_name}"
140-
method = getattr(self, method_name, None)
155+
method = getattr(action_target, method_name, None)
156+
log.debug("ACTION METHOD %r", method)
141157
if method is not None:
142-
await method(tokens)
158+
try:
159+
await method(*params)
160+
except Exception:
161+
log.exception(
162+
f"error in action {destination}.{action_name}{params!r}"
163+
)
143164

144165
async def on_shutdown_request(self, event: events.ShutdownRequest) -> None:
145166
log.debug("shutdown request")
@@ -163,7 +184,7 @@ async def on_mouse_scroll_up(self, event: events.MouseScrollUp) -> None:
163184
async def on_mouse_scroll_down(self, event: events.MouseScrollUp) -> None:
164185
await self.view.post_message(event)
165186

166-
async def action_quit(self, tokens: list[str]) -> None:
187+
async def action_quit(self) -> None:
167188
await self.close_messages()
168189

169190

@@ -191,7 +212,7 @@ async def action_quit(self, tokens: list[str]) -> None:
191212

192213
class MyApp(App):
193214

194-
KEYS = {"q": "quit", "ctrl+c": "quit"}
215+
KEYS = {"q": "quit", "ctrl+c": "quit", "b": "view.toggle('left')"}
195216

196217
async def on_startup(self, event: events.Startup) -> None:
197218
await self.view.mount_all(

src/textual/message_pump.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ async def process_messages(self) -> None:
153153
pending = self.peek_message()
154154
if pending is None or not message.can_batch(pending.message):
155155
break
156-
priority, message = pending
156+
priority, message = await self.get_message()
157157

158158
try:
159159
await self.dispatch_message(message, priority)

src/textual/view.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
from abc import ABC, abstractmethod
4+
from time import time
45
import logging
56
from typing import Optional, Tuple, TYPE_CHECKING
67

@@ -12,6 +13,7 @@
1213

1314
from . import events
1415
from ._context import active_app
16+
from .geometry import Dimensions
1517
from .message import Message
1618
from .message_pump import MessagePump
1719
from .widget import Widget, UpdateMessage
@@ -85,6 +87,7 @@ def __init__(
8587
self.layout = layout
8688
self.mouse_over: MessagePump | None = None
8789
self.focused: Widget | None = None
90+
self.size = Dimensions(0, 0)
8891
self._widgets: set[Widget] = set()
8992
super().__init__()
9093
self.enable_messages(events.Idle)
@@ -114,10 +117,15 @@ async def on_message(self, message: Message) -> None:
114117
for layout, (region, render) in self.layout.map.items():
115118
if layout.renderable is widget:
116119
assert isinstance(widget, Widget)
120+
start = time()
117121
update = widget.render_update(region.x, region.y)
118122
segments = Segments(update)
123+
log.debug(
124+
"RENDER UPDATE %r rendered in %.1fms",
125+
widget,
126+
(time() - start) * 1000.0,
127+
)
119128
self.console.print(segments, end="")
120-
break
121129

122130
async def on_create(self, event: events.Created) -> None:
123131
await self.mount(Header(self.title))
@@ -149,15 +157,22 @@ async def set_focus(self, widget: Optional[Widget]) -> None:
149157
async def on_startup(self, event: events.Startup) -> None:
150158
await self.mount(Header(self.title), slot="header")
151159

152-
async def on_resize(self, event: events.Resize) -> None:
153-
region_map = self.layout._make_region_map(event.width, event.height)
160+
async def layout_update(self) -> None:
161+
if not self.size:
162+
return
163+
width, height = self.size
164+
region_map = self.layout._make_region_map(width, height)
154165
for layout, region in region_map.items():
155166
if isinstance(layout.renderable, Widget):
156167
await layout.renderable.post_message(
157168
events.Resize(self, region.width, region.height)
158169
)
159170
self.app.refresh()
160171

172+
async def on_resize(self, event: events.Resize) -> None:
173+
self.size = Dimensions(event.width, event.height)
174+
await self.layout_update()
175+
161176
async def _on_mouse_move(self, event: events.MouseMove) -> None:
162177
try:
163178
widget, region = self.get_widget_at(event.x, event.y)
@@ -212,3 +227,8 @@ async def forward_input_event(self, event: events.Event) -> None:
212227
else:
213228
if self.focused is not None:
214229
await self.focused.forward_input_event(event)
230+
231+
async def action_toggle(self, layout_name: str) -> None:
232+
visible = self.layout[layout_name].visible
233+
self.layout[layout_name].visible = not visible
234+
await self.layout_update()

src/textual/widget.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
from .message_pump import MessagePump
2626
from .geometry import Dimensions
2727

28+
from time import time
29+
2830
if TYPE_CHECKING:
2931
from .app import App
3032

@@ -34,7 +36,10 @@
3436

3537

3638
class UpdateMessage(Message):
37-
pass
39+
default_priority = 10
40+
41+
def can_batch(self, message: Message) -> bool:
42+
return isinstance(message, UpdateMessage) and message.sender == self.sender
3843

3944

4045
class Reactive(Generic[T]):
@@ -105,10 +110,12 @@ def line_cache(self) -> LineCache:
105110

106111
if self._line_cache is None:
107112
width, height = self.size
113+
start = time()
108114
renderable = self.render(self.console, self.console.options)
109115
self._line_cache = LineCache.from_renderable(
110116
self.console, renderable, width, height
111117
)
118+
log.debug("%.1fms %r render elapsed", (time() - start) * 1000, self)
112119
assert self._line_cache is not None
113120
return self._line_cache
114121

@@ -151,5 +158,5 @@ async def on_event(self, event: events.Event, priority: int) -> None:
151158
await super().on_event(event, priority)
152159

153160
async def on_idle(self, event: events.Idle) -> None:
154-
if self.line_cache.dirty:
161+
if self.line_cache is None or self.line_cache.dirty:
155162
await self.repaint()

0 commit comments

Comments
 (0)