Skip to content

Commit ab14b76

Browse files
committed
add footer
1 parent 3a02044 commit ab14b76

File tree

11 files changed

+136
-65
lines changed

11 files changed

+136
-65
lines changed

poetry.lock

Lines changed: 11 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
[tool.poetry]
22
name = "textual"
3-
version = "0.1.0"
3+
version = "0.1.1"
44
description = "Text User Interface using Rich"
55
authors = ["Will McGugan <[email protected]>"]
66
license = "MIT"
77

88
[tool.poetry.dependencies]
99
python = "^3.7"
10-
rich = "^10.3.0"
10+
rich = "^10.4.0"
11+
typing-extensions = { version = "^3.10.0", python = "<3.8" }
1112

1213
[tool.poetry.dev-dependencies]
13-
rich = {git = "[email protected]:willmcgugan/rich", rev = "pretty-classes"}
14+
# rich = {git = "[email protected]:willmcgugan/rich", rev = "pretty-classes"}
1415

1516
[build-system]
1617
requires = ["poetry-core>=1.0.0"]

src/textual/_line_cache.py

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

3+
34
import logging
45

56
from typing import Iterable
@@ -8,6 +9,7 @@
89
from rich.control import Control
910
from rich.segment import Segment
1011

12+
from ._loop import loop_last
1113

1214
log = logging.getLogger("rich")
1315

@@ -26,7 +28,7 @@ def from_renderable(
2628
height: int,
2729
) -> "LineCache":
2830
options = console.options.update_dimensions(width, height)
29-
lines = console.render_lines(renderable, options, new_lines=True)
31+
lines = console.render_lines(renderable, options)
3032
return cls(lines)
3133

3234
@property
@@ -37,14 +39,21 @@ def __rich_console__(
3739
self, console: Console, options: ConsoleOptions
3840
) -> RenderResult:
3941

42+
new_line = Segment.line()
4043
for line in self.lines:
4144
yield from line
45+
yield new_line
4246

43-
def render(self, x: int, y: int) -> Iterable[Segment]:
47+
def render(self, x: int, y: int, width: int, height: int) -> Iterable[Segment]:
4448
move_to = Control.move_to
45-
for offset_y, (line, dirty) in enumerate(zip(self.lines, self._dirty), y):
49+
lines = self.lines[:height]
50+
new_line = Segment.line()
51+
for last, (offset_y, (line, dirty)) in loop_last(
52+
enumerate(zip(lines, self._dirty), y)
53+
):
4654
if dirty:
4755
yield move_to(x, offset_y).segment
48-
yield from line
49-
56+
yield from Segment.adjust_line_length(line, width)
57+
if not last:
58+
yield new_line
5059
self._dirty[:] = [False] * len(self.lines)

src/textual/_parser.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -167,9 +167,11 @@ class TestParser(Parser[str]):
167167
def parse(
168168
self, on_token: Callable[[str], None]
169169
) -> Generator[Awaitable, str, None]:
170-
# on_token((yield self.read_until("a")))
171-
while data := (yield self.read1()):
172-
print("buffer", repr((yield self.peek_buffer())))
170+
data = yield self.read1()
171+
while True:
172+
data = yield self.read1()
173+
if not data:
174+
break
173175
on_token(data)
174176

175177
test_parser = TestParser()

src/textual/_types.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
from typing import Awaitable, Callable, Optional, Protocol, TYPE_CHECKING
1+
import sys
2+
from typing import Awaitable, Callable, Optional, TYPE_CHECKING
3+
4+
if sys.version_info >= (3, 8):
5+
from typing import Protocol
6+
else:
7+
from typing_extensions import Protocol
8+
29

310
if TYPE_CHECKING:
411
from .events import Event

src/textual/app.py

Lines changed: 42 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,12 @@
3434

3535
LayoutDefinition = "dict[str, Any]"
3636

37-
try:
38-
import uvloop
39-
except ImportError:
40-
pass
41-
else:
42-
uvloop.install()
37+
# try:
38+
# import uvloop
39+
# except ImportError:
40+
# pass
41+
# else:
42+
# uvloop.install()
4343

4444

4545
class ShutdownError(Exception):
@@ -106,6 +106,7 @@ async def _process_messages(self) -> None:
106106
await self.add(self.view)
107107

108108
await self.post_message(events.Startup(sender=self))
109+
109110
try:
110111
driver.start_application_mode()
111112
except Exception:
@@ -115,23 +116,26 @@ async def _process_messages(self) -> None:
115116
await super().process_messages()
116117
finally:
117118
try:
118-
driver.stop_application_mode()
119-
finally:
120-
loop.remove_signal_handler(signal.SIGINT)
119+
if self.children:
121120

122-
if self.children:
121+
async def close_all() -> None:
122+
for child in self.children:
123+
await child.close_messages()
124+
await asyncio.gather(*(child.task for child in self.children))
123125

124-
async def close_all() -> None:
125-
for child in self.children:
126-
await child.close_messages()
127-
await asyncio.gather(*(child.task for child in self.children))
126+
try:
127+
await asyncio.wait_for(close_all(), timeout=5)
128+
except asyncio.TimeoutError as error:
129+
raise ShutdownError(
130+
"Timeout closing messages pump(s)"
131+
) from None
128132

129-
try:
130-
await asyncio.wait_for(close_all(), timeout=5)
131-
except asyncio.TimeoutError as error:
132-
raise ShutdownError("Timeout closing messages pump(s)") from None
133-
134-
self.children.clear()
133+
self.children.clear()
134+
finally:
135+
try:
136+
driver.stop_application_mode()
137+
finally:
138+
loop.remove_signal_handler(signal.SIGINT)
135139

136140
async def add(self, child: MessagePump) -> None:
137141
self.children.add(child)
@@ -222,31 +226,42 @@ async def action_bang(self) -> None:
222226
from logging import FileHandler
223227

224228
from .widgets.header import Header
229+
from .widgets.footer import Footer
225230
from .widgets.window import Window
226231
from .widgets.placeholder import Placeholder
227-
from .scrollbar import ScrollBar
228232

229233
from rich.markdown import Markdown
230234

235+
import os
236+
231237
logging.basicConfig(
232238
level="NOTSET",
233239
format="%(message)s",
234240
datefmt="[%X]",
235241
handlers=[FileHandler("richtui.log")],
236242
)
237243

238-
with open("richreadme.md", "rt") as fh:
239-
readme = Markdown(fh.read(), hyperlinks=True, code_theme="fruity")
240-
241-
from rich import print
242-
243244
class MyApp(App):
244245

245246
KEYS = {"q": "quit", "x": "bang", "ctrl+c": "quit", "b": "view.toggle('left')"}
246247

247248
async def on_startup(self, event: events.Startup) -> None:
249+
footer = Footer()
250+
footer.add_key("b", "Toggle sidebar")
251+
footer.add_key("q", "Quit")
252+
253+
readme_path = os.path.join(
254+
os.path.dirname(os.path.abspath(__file__)), "richreadme.md"
255+
)
256+
257+
with open(readme_path, "rt") as fh:
258+
readme = Markdown(fh.read(), hyperlinks=True, code_theme="fruity")
259+
248260
await self.view.mount_all(
249-
header=Header(self.title), left=ScrollBar(), body=Window(readme)
261+
header=Header(self.title),
262+
left=Placeholder(),
263+
body=Window(readme),
264+
footer=footer,
250265
)
251266

252267
MyApp.run()
File renamed without changes.

src/textual/view.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,8 @@ def __rich_repr__(self) -> RichReprResult:
9494
def __rich_console__(
9595
self, console: Console, options: ConsoleOptions
9696
) -> RenderResult:
97-
segments = console.render(self.layout, options)
97+
width, height = self.size
98+
segments = console.render(self.layout, options.update_dimensions(width, height))
9899
yield from segments
99100

100101
def get_widget_at(self, x: int, y: int) -> Tuple[Widget, LayoutRegion]:
@@ -117,8 +118,8 @@ async def on_message(self, message: Message) -> None:
117118
segments = Segments(update)
118119
self.console.print(segments, end="")
119120

120-
async def on_create(self, event: events.Created) -> None:
121-
await self.mount(Header(self.title))
121+
# async def on_create(self, event: events.Created) -> None:
122+
# await self.mount(Header(self.title))
122123

123124
async def mount(self, widget: Widget, *, slot: str = "main") -> None:
124125
self.layout[slot].update(widget)
@@ -144,8 +145,8 @@ async def set_focus(self, widget: Optional[Widget]) -> None:
144145
self.focused = widget
145146
await widget.post_message(events.Focus(self))
146147

147-
async def on_startup(self, event: events.Startup) -> None:
148-
await self.mount(Header(self.title), slot="header")
148+
# async def on_startup(self, event: events.Startup) -> None:
149+
# await self.mount(Header(self.title), slot="header")
149150

150151
async def layout_update(self) -> None:
151152
if not self.size:

src/textual/widget.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@ def __set__(self, obj: "Widget", value: T) -> None:
6666
obj.require_repaint()
6767

6868

69-
@rich_repr
7069
class Widget(MessagePump):
7170
_count: ClassVar[int] = 0
7271
can_focus: bool = False
@@ -108,7 +107,13 @@ def line_cache(self) -> LineCache:
108107
if self._line_cache is None:
109108
width, height = self.size
110109
start = time()
111-
renderable = self.render(self.console, self.console.options)
110+
try:
111+
renderable = self.render(
112+
self.console, self.console.options.update_width(width)
113+
)
114+
except Exception:
115+
log.exception("error in render")
116+
raise
112117
self._line_cache = LineCache.from_renderable(
113118
self.console, renderable, width, height
114119
)
@@ -126,14 +131,16 @@ async def forward_input_event(self, event: events.Event) -> None:
126131
await self.post_message(event)
127132

128133
async def refresh(self) -> None:
129-
# self._line_cache = None
134+
self._line_cache = None
130135
await self.repaint()
131136

132137
async def repaint(self) -> None:
133138
await self.emit(UpdateMessage(self))
134139

135140
def render_update(self, x: int, y: int) -> Iterable[Segment]:
136-
yield from self.line_cache.render(x, y)
141+
width, height = self.size
142+
log.debug("widget size = %r", self.size)
143+
yield from self.line_cache.render(x, y, width, height)
137144

138145
def render(self, console: Console, options: ConsoleOptions) -> RenderableType:
139146
return Panel(

src/textual/widgets/footer.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from rich.console import Console, ConsoleOptions, RenderableType
2+
from rich.repr import rich_repr, RichReprResult
3+
from rich.text import Text
4+
5+
from .. import events
6+
from ..widget import Widget
7+
8+
9+
class Footer(Widget):
10+
def __init__(self) -> None:
11+
self.keys: list[tuple[str, str]] = []
12+
super().__init__()
13+
14+
def __rich_repr__(self) -> RichReprResult:
15+
yield "footer"
16+
17+
def add_key(self, key: str, label: str) -> None:
18+
self.keys.append((key, label))
19+
20+
def render(self, console: Console, options: ConsoleOptions) -> RenderableType:
21+
22+
text = Text(
23+
style="white on dark_green",
24+
no_wrap=True,
25+
overflow="ellipsis",
26+
justify="left",
27+
end="",
28+
)
29+
for key, label in self.keys:
30+
text.append(f" {key.upper()} ", style="default on default")
31+
text.append(f" {label} ")
32+
return text

0 commit comments

Comments
 (0)