Skip to content

Commit cb56ec7

Browse files
authored
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
1 parent 3b36864 commit cb56ec7

File tree

9 files changed

+94
-10
lines changed

9 files changed

+94
-10
lines changed

CHANGELOG.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Ability to change terminal window title https://github.com/Textualize/rich/pull/2200
13+
1014
### Fixed
1115

1216
- 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
2529

2630
- 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
2731
- SVG export functionality https://github.com/Textualize/rich/pull/2101
28-
29-
### Added
30-
3132
- Adding Indonesian translation
3233

3334
### Fixed

rich/_windows_renderer.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,6 @@ def legacy_windows_render(buffer: Iterable[Segment], term: LegacyWindowsTerm) ->
5151
term.erase_start_of_line()
5252
elif mode == 2:
5353
term.erase_line()
54+
elif control_type == ControlType.SET_WINDOW_TITLE:
55+
_, title = cast(Tuple[ControlType, str], control_code)
56+
term.set_title(title)

rich/console.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1181,6 +1181,38 @@ def is_alt_screen(self) -> bool:
11811181
"""
11821182
return self._is_alt_screen
11831183

1184+
def set_window_title(self, title: str) -> bool:
1185+
"""Set the title of the console terminal window.
1186+
1187+
Warning: There is no means within Rich of "resetting" the window title to its
1188+
previous value, meaning the title you set will persist even after your application
1189+
exits.
1190+
1191+
``fish`` shell resets the window title before and after each command by default,
1192+
negating this issue. Windows Terminal and command prompt will also reset the title for you.
1193+
Most other shells and terminals, however, do not do this.
1194+
1195+
Some terminals may require configuration changes before you can set the title.
1196+
Some terminals may not support setting the title at all.
1197+
1198+
Other software (including the terminal itself, the shell, custom prompts, plugins, etc.)
1199+
may also set the terminal window title. This could result in whatever value you write
1200+
using this method being overwritten.
1201+
1202+
Args:
1203+
title (str): The new title of the terminal window.
1204+
1205+
Returns:
1206+
bool: True if the control code to change the terminal title was
1207+
written, otherwise False. Note that a return value of True
1208+
does not guarantee that the window title has actually changed,
1209+
since the feature may be unsupported/disabled in some terminals.
1210+
"""
1211+
if self.is_terminal:
1212+
self.control(Control.title(title))
1213+
return True
1214+
return False
1215+
11841216
def screen(
11851217
self, hide_cursor: bool = True, style: Optional[StyleType] = None
11861218
) -> "ScreenContext":

rich/control.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from typing import Callable, Dict, Iterable, List, TYPE_CHECKING, Union
1+
import time
2+
from typing import TYPE_CHECKING, Callable, Dict, Iterable, List, Union
23

34
from .segment import ControlCode, ControlType, Segment
45

@@ -30,6 +31,7 @@
3031
ControlType.CURSOR_MOVE_TO_COLUMN: lambda param: f"\x1b[{param+1}G",
3132
ControlType.ERASE_IN_LINE: lambda param: f"\x1b[{param}K",
3233
ControlType.CURSOR_MOVE_TO: lambda x, y: f"\x1b[{y+1};{x+1}H",
34+
ControlType.SET_WINDOW_TITLE: lambda title: f"\x1b]0;{title}\x07",
3335
}
3436

3537

@@ -147,6 +149,15 @@ def alt_screen(cls, enable: bool) -> "Control":
147149
else:
148150
return cls(ControlType.DISABLE_ALT_SCREEN)
149151

152+
@classmethod
153+
def title(cls, title: str) -> "Control":
154+
"""Set the terminal window title
155+
156+
Args:
157+
title (str): The new terminal window title
158+
"""
159+
return cls((ControlType.SET_WINDOW_TITLE, title))
160+
150161
def __str__(self) -> str:
151162
return self.segment.text
152163

@@ -172,4 +183,11 @@ def strip_control_codes(
172183

173184

174185
if __name__ == "__main__": # pragma: no cover
175-
print(strip_control_codes("hello\rWorld"))
186+
from rich.console import Console
187+
188+
console = Console()
189+
console.print("Look at the title of your terminal window ^")
190+
# console.print(Control((ControlType.SET_WINDOW_TITLE, "Hello, world!")))
191+
for i in range(10):
192+
console.set_window_title("🚀 Loading" + "." * i)
193+
time.sleep(0.5)

rich/segment.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,13 @@ class ControlType(IntEnum):
4949
CURSOR_MOVE_TO_COLUMN = 13
5050
CURSOR_MOVE_TO = 14
5151
ERASE_IN_LINE = 15
52+
SET_WINDOW_TITLE = 16
5253

5354

5455
ControlCode = Union[
55-
Tuple[ControlType], Tuple[ControlType, int], Tuple[ControlType, int, int]
56+
Tuple[ControlType],
57+
Tuple[ControlType, Union[int, str]],
58+
Tuple[ControlType, int, int],
5659
]
5760

5861

tests/test_console.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -877,6 +877,18 @@ def test_is_alt_screen():
877877
assert not console.is_alt_screen
878878

879879

880+
def test_set_console_title():
881+
console = Console(force_terminal=True, _environ={})
882+
if console.legacy_windows:
883+
return
884+
885+
with console.capture() as captured:
886+
console.set_window_title("hello")
887+
888+
result = captured.get()
889+
assert result == "\x1b]0;hello\x07"
890+
891+
880892
def test_update_screen():
881893
console = Console(force_terminal=True, width=20, height=5, _environ={})
882894
if console.legacy_windows:

tests/test_control.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from rich.control import Control, strip_control_codes
2-
from rich.segment import Segment, ControlType
2+
from rich.segment import ControlType, Segment
33

44

55
def test_control():
@@ -45,3 +45,12 @@ def test_move_to_column():
4545
None,
4646
[(ControlType.CURSOR_MOVE_TO_COLUMN, 10), (ControlType.CURSOR_UP, 20)],
4747
)
48+
49+
50+
def test_title():
51+
control_segment = Control.title("hello").segment
52+
assert control_segment == Segment(
53+
"\x1b]0;hello\x07",
54+
None,
55+
[(ControlType.SET_WINDOW_TITLE, "hello")],
56+
)

tests/test_live.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44

55
# import pytest
66
from rich.console import Console
7-
from rich.text import Text
87
from rich.live import Live
8+
from rich.text import Text
99

1010

1111
def create_capture_console(
@@ -116,8 +116,6 @@ def test_growing_display_overflow_visible() -> None:
116116

117117
def test_growing_display_autorefresh() -> None:
118118
"""Test generating a table but using auto-refresh from threading"""
119-
console = create_capture_console()
120-
121119
console = create_capture_console(height=5)
122120
console.begin_capture()
123121
with Live(console=console, auto_refresh=True, vertical_overflow="visible") as live:

tests/test_windows_renderer.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,3 +131,11 @@ def test_control_cursor_move_to_column(legacy_term_mock):
131131
legacy_windows_render(buffer, legacy_term_mock)
132132

133133
legacy_term_mock.move_cursor_to_column.assert_called_once_with(2)
134+
135+
136+
def test_control_set_terminal_window_title(legacy_term_mock):
137+
buffer = [Segment("", None, [(ControlType.SET_WINDOW_TITLE, "Hello, world!")])]
138+
139+
legacy_windows_render(buffer, legacy_term_mock)
140+
141+
legacy_term_mock.set_title.assert_called_once_with("Hello, world!")

0 commit comments

Comments
 (0)