Skip to content

Commit

Permalink
Setting terminal window title (#2200)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
darrenburns committed Apr 19, 2022
1 parent 3b36864 commit cb56ec7
Show file tree
Hide file tree
Showing 9 changed files with 94 additions and 10 deletions.
7 changes: 4 additions & 3 deletions CHANGELOG.md
Expand Up @@ -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)
Expand All @@ -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
Expand Down
3 changes: 3 additions & 0 deletions rich/_windows_renderer.py
Expand Up @@ -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)
32 changes: 32 additions & 0 deletions rich/console.py
Expand Up @@ -1181,6 +1181,38 @@ def is_alt_screen(self) -> bool:
"""
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":
Expand Down
22 changes: 20 additions & 2 deletions rich/control.py
@@ -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

Expand Down Expand Up @@ -30,6 +31,7 @@
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",
}


Expand Down Expand Up @@ -147,6 +149,15 @@ def alt_screen(cls, enable: bool) -> "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

Expand All @@ -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)
5 changes: 4 additions & 1 deletion rich/segment.py
Expand Up @@ -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],
]


Expand Down
12 changes: 12 additions & 0 deletions tests/test_console.py
Expand Up @@ -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:
Expand Down
11 changes: 10 additions & 1 deletion tests/test_control.py
@@ -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():
Expand Down Expand Up @@ -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 changes: 1 addition & 3 deletions tests/test_live.py
Expand Up @@ -4,8 +4,8 @@

# 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(
Expand Down Expand Up @@ -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:
Expand Down
8 changes: 8 additions & 0 deletions tests/test_windows_renderer.py
Expand Up @@ -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!")

0 comments on commit cb56ec7

Please sign in to comment.