mirror of
https://github.com/Textualize/rich.git
synced 2026-02-06 10:58:48 +00:00
Setting terminal window title (#2200)
* Adding tests for setting console title * Add Windows note * Update changelog regarding changing terminal window title * Add test for window title control code -> legacy windows conversion * Fix docstring typo
This commit is contained in:
parent
3b36864ad0
commit
cb56ec7ebb
@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Ability to change terminal window title https://github.com/Textualize/rich/pull/2200
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fall back to `sys.__stderr__` on POSIX systems when trying to get the terminal size (fix issues when Rich is piped to another process)
|
||||
@ -25,9 +29,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
- Progress.open and Progress.wrap_file method to track the progress while reading from a file or file-like object https://github.com/willmcgugan/rich/pull/1759
|
||||
- SVG export functionality https://github.com/Textualize/rich/pull/2101
|
||||
|
||||
### Added
|
||||
|
||||
- Adding Indonesian translation
|
||||
|
||||
### Fixed
|
||||
|
||||
@ -51,3 +51,6 @@ def legacy_windows_render(buffer: Iterable[Segment], term: LegacyWindowsTerm) ->
|
||||
term.erase_start_of_line()
|
||||
elif mode == 2:
|
||||
term.erase_line()
|
||||
elif control_type == ControlType.SET_WINDOW_TITLE:
|
||||
_, title = cast(Tuple[ControlType, str], control_code)
|
||||
term.set_title(title)
|
||||
|
||||
@ -1181,6 +1181,38 @@ class Console:
|
||||
"""
|
||||
return self._is_alt_screen
|
||||
|
||||
def set_window_title(self, title: str) -> bool:
|
||||
"""Set the title of the console terminal window.
|
||||
|
||||
Warning: There is no means within Rich of "resetting" the window title to its
|
||||
previous value, meaning the title you set will persist even after your application
|
||||
exits.
|
||||
|
||||
``fish`` shell resets the window title before and after each command by default,
|
||||
negating this issue. Windows Terminal and command prompt will also reset the title for you.
|
||||
Most other shells and terminals, however, do not do this.
|
||||
|
||||
Some terminals may require configuration changes before you can set the title.
|
||||
Some terminals may not support setting the title at all.
|
||||
|
||||
Other software (including the terminal itself, the shell, custom prompts, plugins, etc.)
|
||||
may also set the terminal window title. This could result in whatever value you write
|
||||
using this method being overwritten.
|
||||
|
||||
Args:
|
||||
title (str): The new title of the terminal window.
|
||||
|
||||
Returns:
|
||||
bool: True if the control code to change the terminal title was
|
||||
written, otherwise False. Note that a return value of True
|
||||
does not guarantee that the window title has actually changed,
|
||||
since the feature may be unsupported/disabled in some terminals.
|
||||
"""
|
||||
if self.is_terminal:
|
||||
self.control(Control.title(title))
|
||||
return True
|
||||
return False
|
||||
|
||||
def screen(
|
||||
self, hide_cursor: bool = True, style: Optional[StyleType] = None
|
||||
) -> "ScreenContext":
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
from typing import Callable, Dict, Iterable, List, TYPE_CHECKING, Union
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Callable, Dict, Iterable, List, Union
|
||||
|
||||
from .segment import ControlCode, ControlType, Segment
|
||||
|
||||
@ -30,6 +31,7 @@ CONTROL_CODES_FORMAT: Dict[int, Callable[..., str]] = {
|
||||
ControlType.CURSOR_MOVE_TO_COLUMN: lambda param: f"\x1b[{param+1}G",
|
||||
ControlType.ERASE_IN_LINE: lambda param: f"\x1b[{param}K",
|
||||
ControlType.CURSOR_MOVE_TO: lambda x, y: f"\x1b[{y+1};{x+1}H",
|
||||
ControlType.SET_WINDOW_TITLE: lambda title: f"\x1b]0;{title}\x07",
|
||||
}
|
||||
|
||||
|
||||
@ -147,6 +149,15 @@ class Control:
|
||||
else:
|
||||
return cls(ControlType.DISABLE_ALT_SCREEN)
|
||||
|
||||
@classmethod
|
||||
def title(cls, title: str) -> "Control":
|
||||
"""Set the terminal window title
|
||||
|
||||
Args:
|
||||
title (str): The new terminal window title
|
||||
"""
|
||||
return cls((ControlType.SET_WINDOW_TITLE, title))
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.segment.text
|
||||
|
||||
@ -172,4 +183,11 @@ def strip_control_codes(
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
print(strip_control_codes("hello\rWorld"))
|
||||
from rich.console import Console
|
||||
|
||||
console = Console()
|
||||
console.print("Look at the title of your terminal window ^")
|
||||
# console.print(Control((ControlType.SET_WINDOW_TITLE, "Hello, world!")))
|
||||
for i in range(10):
|
||||
console.set_window_title("🚀 Loading" + "." * i)
|
||||
time.sleep(0.5)
|
||||
|
||||
@ -49,10 +49,13 @@ class ControlType(IntEnum):
|
||||
CURSOR_MOVE_TO_COLUMN = 13
|
||||
CURSOR_MOVE_TO = 14
|
||||
ERASE_IN_LINE = 15
|
||||
SET_WINDOW_TITLE = 16
|
||||
|
||||
|
||||
ControlCode = Union[
|
||||
Tuple[ControlType], Tuple[ControlType, int], Tuple[ControlType, int, int]
|
||||
Tuple[ControlType],
|
||||
Tuple[ControlType, Union[int, str]],
|
||||
Tuple[ControlType, int, int],
|
||||
]
|
||||
|
||||
|
||||
|
||||
@ -877,6 +877,18 @@ def test_is_alt_screen():
|
||||
assert not console.is_alt_screen
|
||||
|
||||
|
||||
def test_set_console_title():
|
||||
console = Console(force_terminal=True, _environ={})
|
||||
if console.legacy_windows:
|
||||
return
|
||||
|
||||
with console.capture() as captured:
|
||||
console.set_window_title("hello")
|
||||
|
||||
result = captured.get()
|
||||
assert result == "\x1b]0;hello\x07"
|
||||
|
||||
|
||||
def test_update_screen():
|
||||
console = Console(force_terminal=True, width=20, height=5, _environ={})
|
||||
if console.legacy_windows:
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
from rich.control import Control, strip_control_codes
|
||||
from rich.segment import Segment, ControlType
|
||||
from rich.segment import ControlType, Segment
|
||||
|
||||
|
||||
def test_control():
|
||||
@ -45,3 +45,12 @@ def test_move_to_column():
|
||||
None,
|
||||
[(ControlType.CURSOR_MOVE_TO_COLUMN, 10), (ControlType.CURSOR_UP, 20)],
|
||||
)
|
||||
|
||||
|
||||
def test_title():
|
||||
control_segment = Control.title("hello").segment
|
||||
assert control_segment == Segment(
|
||||
"\x1b]0;hello\x07",
|
||||
None,
|
||||
[(ControlType.SET_WINDOW_TITLE, "hello")],
|
||||
)
|
||||
|
||||
@ -4,8 +4,8 @@ from typing import Optional
|
||||
|
||||
# import pytest
|
||||
from rich.console import Console
|
||||
from rich.text import Text
|
||||
from rich.live import Live
|
||||
from rich.text import Text
|
||||
|
||||
|
||||
def create_capture_console(
|
||||
@ -116,8 +116,6 @@ def test_growing_display_overflow_visible() -> None:
|
||||
|
||||
def test_growing_display_autorefresh() -> None:
|
||||
"""Test generating a table but using auto-refresh from threading"""
|
||||
console = create_capture_console()
|
||||
|
||||
console = create_capture_console(height=5)
|
||||
console.begin_capture()
|
||||
with Live(console=console, auto_refresh=True, vertical_overflow="visible") as live:
|
||||
|
||||
@ -131,3 +131,11 @@ def test_control_cursor_move_to_column(legacy_term_mock):
|
||||
legacy_windows_render(buffer, legacy_term_mock)
|
||||
|
||||
legacy_term_mock.move_cursor_to_column.assert_called_once_with(2)
|
||||
|
||||
|
||||
def test_control_set_terminal_window_title(legacy_term_mock):
|
||||
buffer = [Segment("", None, [(ControlType.SET_WINDOW_TITLE, "Hello, world!")])]
|
||||
|
||||
legacy_windows_render(buffer, legacy_term_mock)
|
||||
|
||||
legacy_term_mock.set_title.assert_called_once_with("Hello, world!")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user