Skip to content

Commit 9f5e653

Browse files
Merge branch 'main' into batch-async-context-manager
2 parents 808f536 + ba17dfb commit 9f5e653

File tree

5 files changed

+42
-13
lines changed

5 files changed

+42
-13
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,17 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
77

88
## Unreleased
99

10+
### Changed
11+
12+
- Textual now writes to stderr rather than stdout
13+
1014
### Added
1115

16+
- Added an `asyncio` lock attribute `Widget.lock` to be used to synchronize widget state https://github.com/Textualize/textual/issues/4134
1217
- Added support for environment variable `TEXTUAL_ANIMATIONS` to control what animations Textual displays https://github.com/Textualize/textual/pull/4062
1318
- Add attribute `App.animation_level` to control whether animations on that app run or not https://github.com/Textualize/textual/pull/4062
19+
- Added support for a `TEXTUAL_SCREENSHOT_LOCATION` environment variable to specify the location of an automated screenshot https://github.com/Textualize/textual/pull/4181/
20+
- Added support for a `TEXTUAL_SCREENSHOT_FILENAME` environment variable to specify the filename of an automated screenshot https://github.com/Textualize/textual/pull/4181/
1421
- Added an `asyncio` lock attribute `Widget.lock` to be used to synchronize widget state https://github.com/Textualize/textual/issues/4134
1522
- `Widget.remove_children` now accepts a CSS selector to specify which children to remove https://github.com/Textualize/textual/pull/4183
1623
- `Widget.batch` combines widget locking and app update batching https://github.com/Textualize/textual/pull/4183

src/textual/app.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1153,7 +1153,7 @@ def export_screenshot(self, *, title: str | None = None) -> str:
11531153
def save_screenshot(
11541154
self,
11551155
filename: str | None = None,
1156-
path: str = "./",
1156+
path: str | None = None,
11571157
time_format: str | None = None,
11581158
) -> str:
11591159
"""Save an SVG screenshot of the current screen.
@@ -1168,7 +1168,8 @@ def save_screenshot(
11681168
Returns:
11691169
Filename of screenshot.
11701170
"""
1171-
if filename is None:
1171+
path = path or "./"
1172+
if not filename:
11721173
if time_format is None:
11731174
dt = datetime.now().isoformat()
11741175
else:
@@ -2397,7 +2398,10 @@ async def _ready(self) -> None:
23972398

23982399
async def take_screenshot() -> None:
23992400
"""Take a screenshot and exit."""
2400-
self.save_screenshot()
2401+
self.save_screenshot(
2402+
path=constants.SCREENSHOT_LOCATION,
2403+
filename=constants.SCREENSHOT_FILENAME,
2404+
)
24012405
self.exit()
24022406

24032407
if constants.SCREENSHOT_DELAY >= 0:

src/textual/constants.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,12 @@ def _get_textual_animations() -> AnimationLevel:
9595
SCREENSHOT_DELAY: Final[int] = _get_environ_int("TEXTUAL_SCREENSHOT", -1)
9696
"""Seconds delay before taking screenshot."""
9797

98+
SCREENSHOT_LOCATION: Final[str | None] = get_environ("TEXTUAL_SCREENSHOT_LOCATION")
99+
"""The location where screenshots should be written."""
100+
101+
SCREENSHOT_FILENAME: Final[str | None] = get_environ("TEXTUAL_SCREENSHOT_FILENAME")
102+
"""The filename to use for the screenshot."""
103+
98104
PRESS: Final[str] = get_environ("TEXTUAL_PRESS", "")
99105
"""Keys to automatically press."""
100106

src/textual/drivers/linux_driver.py

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ def __init__(
4242
size: Initial size of the terminal or `None` to detect.
4343
"""
4444
super().__init__(app, debug=debug, size=size)
45-
self._file = sys.__stdout__
46-
self.fileno = sys.stdin.fileno()
45+
self._file = sys.__stderr__
46+
self.fileno = sys.__stdin__.fileno()
4747
self.attrs_before: list[Any] | None = None
4848
self.exit_event = Event()
4949
self._key_thread: Thread | None = None
@@ -259,7 +259,19 @@ def _request_terminal_sync_mode_support(self) -> None:
259259

260260
@classmethod
261261
def _patch_lflag(cls, attrs: int) -> int:
262-
return attrs & ~(termios.ECHO | termios.ICANON | termios.IEXTEN | termios.ISIG)
262+
"""Patch termios lflag.
263+
264+
Args:
265+
attributes: New set attributes.
266+
267+
Returns:
268+
New lflag.
269+
270+
"""
271+
# if TEXTUAL_ALLOW_SIGNALS env var is set, then allow Ctrl+C to send signals
272+
ISIG = 0 if os.environ.get("TEXTUAL_ALLOW_SIGNALS") else termios.ISIG
273+
274+
return attrs & ~(termios.ECHO | termios.ICANON | termios.IEXTEN | ISIG)
263275

264276
@classmethod
265277
def _patch_iflag(cls, attrs: int) -> int:
@@ -328,15 +340,16 @@ def _run_input_thread(self) -> None:
328340

329341
def run_input_thread(self) -> None:
330342
"""Wait for input and dispatch events."""
331-
selector = selectors.DefaultSelector()
343+
selector = selectors.SelectSelector()
332344
selector.register(self.fileno, selectors.EVENT_READ)
333345

334346
fileno = self.fileno
347+
EVENT_READ = selectors.EVENT_READ
335348

336349
def more_data() -> bool:
337350
"""Check if there is more data to parse."""
338-
EVENT_READ = selectors.EVENT_READ
339-
for key, events in selector.select(0.01):
351+
352+
for _key, events in selector.select(0.01):
340353
if events & EVENT_READ:
341354
return True
342355
return False
@@ -347,14 +360,15 @@ def more_data() -> bool:
347360
utf8_decoder = getincrementaldecoder("utf-8")().decode
348361
decode = utf8_decoder
349362
read = os.read
350-
EVENT_READ = selectors.EVENT_READ
351363

352364
try:
353365
while not self.exit_event.is_set():
354366
selector_events = selector.select(0.1)
355367
for _selector_key, mask in selector_events:
356368
if mask & EVENT_READ:
357-
unicode_data = decode(read(fileno, 1024))
369+
unicode_data = decode(
370+
read(fileno, 1024), final=self.exit_event.is_set()
371+
)
358372
for event in feed(unicode_data):
359373
self.process_event(event)
360374
finally:

src/textual/widgets/_tabs.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -658,7 +658,6 @@ def _activate_tab(self, tab: Tab) -> None:
658658
self.query("#tabs-list Tab.-active").remove_class("-active")
659659
tab.add_class("-active")
660660
self.active = tab.id or ""
661-
self.query_one("#tabs-scroll").scroll_to_center(tab, force=True)
662661

663662
def _on_underline_clicked(self, event: Underline.Clicked) -> None:
664663
"""The underline was clicked.
@@ -714,7 +713,6 @@ def _move_tab(self, direction: int) -> None:
714713
tab_count = len(tabs)
715714
new_tab_index = (tabs.index(active_tab) + direction) % tab_count
716715
self.active = tabs[new_tab_index].id or ""
717-
self._scroll_active_tab()
718716

719717
def _on_tab_disabled(self, event: Tab.Disabled) -> None:
720718
"""Re-post the disabled message."""

0 commit comments

Comments
 (0)