Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Setting terminal window title #2200

Merged
merged 6 commits into from Apr 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great! Did you test this on the Windows laptop?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did yeah, works well there. It even automatically resets the title on both Windows Terminal and legacy Windows, so it's actually even better.

_, 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!")