Skip to content

Commit 1287f8f

Browse files
authored
Update to blessed 1.42 (PR #41)
2 parents 5ea9352 + 4a09a5c commit 1287f8f

13 files changed

Lines changed: 216 additions & 86 deletions

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ repos:
2121
additional_dependencies: [
2222
"numpy>=1.20",
2323
"asyncssh>=2.9",
24-
"blessed>=1.37.0",
24+
"blessed>=1.42.0",
2525
"types-setuptools",
2626
"pytest",
2727
]

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -182,30 +182,30 @@ The table below sums up my findings when I tried the most common terminal emulat
182182
| Ghostty | Excellent | 24-bit colors | Good | Yes | 60 FPS | |
183183
| Kitty | Excellent | 24-bit colors | Good | Yes | 60 FPS | |
184184
| foot | Excellent | 24-bit colors | Good | Yes | 60 FPS | |
185-
| Alacritty | Excellent | 24-bit colors | Good | Yes | 60 FPS | |
186185
| Rio | Excellent | 24-bit colors | Good | Yes | 60 FPS | |
186+
| Contour | Excellent | 24-bit colors | Good | Yes | 60 FPS | [Download latest for kitty support](https://github.com/contour-terminal/contour/releases) |
187+
| Alacritty | Good | 24-bit colors | Good | No* | 60 FPS | *Fails kitty detection [due to reported bug](https://github.com/alacritty/alacritty/pull/8953) |
187188
| Konsole | Good | 24-bit colors | Good | No | 60 FPS | |
188189
| Gnome terminal | Good | 24-bit colors | Good | No | 60 FPS | |
189190
| Terminator | Good | 24-bit colors | Good | No | 60 FPS | |
190-
| XTerm | Good | 24-bit colors | Good | No | 60 FPS | No resize shortcuts, launch as ``xterm -tn xterm-256color`` |
191+
| XTerm | Good | 24-bit colors | Good | No | 60 FPS | Ctrl+Right click to resize, "Unreadable" locks up XTerm |
191192
| Rxvt | Good | 24-bit colors | Good | No | 60 FPS | No resize shortcuts |
193+
| Terminology | Good | 24-bit colors | Light misalignments | No | 60 FPS | Font sizes under ~9pt create horizontal line artifacts |
192194
| Termit | Ok | 24-bit colors | Good | No | 60 FPS | No window title |
193195
| Mlterm | Ok | 24-bit colors | Light misalignments | No | 60 FPS | No resize shortcuts |
194-
| Terminology | Ok | 24-bit colors | Possible misalignments | No | 30 FPS | Weird colors |
195-
| Contour | Bad | 24-bit colors | Good | Broken | 60 FPS | [Bug (no release event!)](https://github.com/contour-terminal/contour/pull/1924) |
196196

197197
About MacOS:
198198

199199
| MacOS | Status | Colors | Unicode rendering | Kitty keyboard protocol | Performance | Comments |
200200
|------------------|------------|---------------|---------------------------|-------------------------|-------------|--------------------------|
201201
| iTerm2 | Excellent | 24-bit colors | Good | Yes | 60 FPS | |
202-
| Terminal | Bad | 24-bit colors | Bad--adjust font spacing! | No | 30 FPS | A bit jittery |
202+
| Terminal.app | Bad | 24-bit colors | Bad--adjust font spacing! | No | 30 FPS | A bit jittery |
203203

204204
About Windows:
205205

206206
| Windows | Status | Colors | Unicode rendering | Kitty keyboard protocol | Performance | Comments |
207207
|--------------------|------------|---------------|------------------------|-------------------------|-------------|--------------------------|
208-
| Windows terminal | Good | 24-bit colors | Good | Coming Soon | 60 FPS | [Download Preview for kitty support)](https://github.com/microsoft/terminal/releases) |
208+
| Windows terminal | Excellent | 24-bit colors | Good | Yes | 60 FPS | [Download latest for kitty support)](https://github.com/microsoft/terminal/releases) |
209209
| Cmder | Unplayable | 24-bit colors | Good | Yes | 2 FPS | No window title |
210210
| Terminus | Unplayable | 24-bit colors | Misalignments | No | 10 FPS | |
211211
| Command prompt | Bad | 24-bit colors | Good | No | 1 FPS | Slow/Unresponsive |

gambaterm/colors.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,23 @@ def report(self) -> str:
2424
"""Return a human-readable report of the color mode."""
2525
if self == ColorMode.COULD_NOT_DETECT:
2626
return "Could not detect color mode"
27+
if self == ColorMode.HAS_24_BIT_COLOR:
28+
return "True color"
29+
return f"{self.number_of_colors} colors"
30+
31+
@property
32+
def number_of_colors(self) -> int:
33+
"""Return a human-readable report of the color mode."""
34+
if self == ColorMode.COULD_NOT_DETECT:
35+
return 0
2736
if self == ColorMode.HAS_2_BIT_COLOR:
28-
return "4 colors"
37+
return 4
2938
if self == ColorMode.HAS_4_BIT_COLOR:
30-
return "16 colors"
39+
return 16
3140
if self == ColorMode.HAS_8_BIT_COLOR:
32-
return "256 colors"
41+
return 256
3342
if self == ColorMode.HAS_24_BIT_COLOR:
34-
return "True color"
43+
return 1 << 24
3544
assert False
3645

3746

gambaterm/keyboard_input.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
Here is a list of terminals known to support this protocol:
2323
2424
- kitty https://sw.kovidgoyal.net/kitty/
25-
- alacritty https://alacritty.org/
2625
- ghostty https://ghostty.org/
2726
- foot https://codeberg.org/dnkl/foot
2827
- iTerm2 https://iterm2.com/
@@ -134,11 +133,20 @@ def pop_keystrokes(self) -> list[Keystroke]:
134133
def is_kitty_keyboard_protocol_supported(
135134
term: Terminal, timeout: float | None = None
136135
) -> bool:
137-
"""Check if the terminal supports the kitty keyboard protocol.
138-
139-
Some terminals (e.g. last release of Contour) responds to the kitty keyboard query but ignore
140-
the flags we set, so we verify that report_events is actually enabled after requesting it.
141-
"""
136+
"""Check if the terminal supports the kitty keyboard protocol."""
137+
# Some terminals (eg. last release of Contour) responds to the kitty keyboard query but ignores
138+
# the modes that we set, it is not fully implementing them. And so we verify that report_events
139+
# is actually enabled after requesting it.
140+
#
141+
# Other terminals (eg. last release of Alacritty), *do* support the kitty keyboard protocol
142+
# flags that set, but fail to accurately report their state!
143+
# https://github.com/alacritty/alacritty/pull/8953 -- it's too bad alacritty also doesn't
144+
# support XTVERSION either, or we could conditionally return True for a version range!
145+
#
146+
# In a sense, we tradeoff: "ensure contour is not wrongly detected" (report_events not
147+
# implemented) for Alacritty is wrongly detected as missing support for kitty (report_events not
148+
# reported due to bug). I hope that Alacritty will accept the reported bug and next release will
149+
# be OK.
142150
state = term.get_kitty_keyboard_state(timeout=timeout)
143151
if state is None:
144152
return False

gambaterm/remote_terminal.py

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,16 @@
1111
from blessed import Terminal as BlessedTerminal
1212
from blessed.terminal import WINSZ
1313

14-
# Python's curses.setupterm() can only be called once per process — subsequent
15-
# calls with a different terminal type are silently ignored. Since the SSH
16-
# server handles multiple concurrent connections in threads, all RemoteTerminal
17-
# instances share whatever terminal type was initialized first by the local
18-
# Terminal(). We hardcode 'xterm-256color' as the kind since:
19-
# 1. It's universally compatible with modern terminals
20-
# 2. We use standard VT100/ANSI escape codes directly, not terminfo caps
21-
# 3. It avoids issues where the first client's TERM value differs from subsequent
22-
REMOTE_TERMINAL_TYPE = "xterm-256color"
23-
2414

2515
class RemoteTerminal(BlessedTerminal):
2616
"""A blessed Terminal subclass for remote streams (SSH, telnet).
2717
2818
Stubs raw/cbreak mode (the remote connection is already raw) and
29-
overrides size detection to use values provided by the server.
19+
overrides size detection to use server protocol-negotiated values.
20+
21+
Callers should invoke ``get_xtgettcap()`` after initialization
22+
to probe the terminal's true capabilities once the connection
23+
is fully established.
3024
"""
3125

3226
def __init__(
@@ -35,18 +29,41 @@ def __init__(
3529
keyboard_fd: int,
3630
rows: int,
3731
columns: int,
32+
kind: str | None = None,
3833
) -> None:
3934
self._rows = rows
4035
self._columns = columns
41-
super().__init__(kind=REMOTE_TERMINAL_TYPE, stream=stream, force_styling=True)
42-
# Blessed only sets _keyboard_fd when stream is sys.__stdout__, so
43-
# for remote pipes we must set it and initialize the decoder manually
44-
self._keyboard_fd = keyboard_fd # type: ignore[assignment]
36+
self._remote_keyboard_fd = keyboard_fd
37+
super().__init__(
38+
kind=kind,
39+
stream=stream,
40+
force_styling=True,
41+
kind_fallback="xterm-256color",
42+
)
43+
# wire `_keyboard_fd` and enable `_is_a_tty` *after* class initialization.
44+
self._keyboard_fd = self._remote_keyboard_fd # type: ignore[assignment]
45+
self._is_a_tty = True
4546
self._keyboard_decoder = codecs.getincrementaldecoder("UTF-8")()
4647

47-
@property
48-
def is_a_tty(self) -> bool:
49-
return True
48+
def probe_xtgettcap(self, timeout: float = 1.0) -> None:
49+
"""
50+
Probe terminal capabilities via XTGETTCAP and apply results.
51+
52+
This allows to improved 'number_of_colors' detection, and, to "overlay" capabilities not
53+
found in jinxed terminfo database but detected by XTGETTCAP: 'blink', 'sitm', 'ritm',
54+
'cvvis', 'Smulx', 'Setulc', 'Ms', the same way that blessed.Terminal() would have but we
55+
is_a_tty was detected False when we initialized it.
56+
57+
This method is not called or used by gambaterm-ssh or gambaterm-telnet, because the above
58+
capabilities are not used and kitty keyboard support pretty reliably suggests 24-bit color
59+
support.
60+
"""
61+
self._xtgettcap_cache = self._Terminal__init__xtgettcap() # type: ignore[assignment]
62+
self.number_of_colors = self._Terminal__init__color_capabilities() # type: ignore[assignment]
63+
if self._xtgettcap_cache.supported and self.does_styling:
64+
self._jinxed_term.overlay_capabilities(
65+
**self._xtgettcap_cache.make_jinxed_capabilities()
66+
)
5067

5168
@contextlib.contextmanager
5269
def raw(self) -> Generator[None, None, None]:

gambaterm/run.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ def run(
162162
height, width = new_height, new_width
163163
refx, refy = get_ref(width, height, console)
164164
color_mode = new_color_mode
165+
term.number_of_colors = new_color_mode.number_of_colors
165166
last_frame.fill(0)
166167

167168
# Render frame with synchronized output mode (DEC 2026) to prevent flickering

gambaterm/ssh.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ async def ssh_process_handler(process: SSHServerProcess[str]) -> int:
152152
terminal_type,
153153
executor,
154154
),
155+
terminal_type=terminal_type,
155156
)
156157

157158

@@ -207,11 +208,16 @@ def ssh_terminal_handler(
207208
else:
208209
assert False
209210

210-
# Kitty keyboard protocol implies 24-bit color support
211+
# It is possible, here, to probe XTGETTCAP which helps correct terminal.number_of_colors using
212+
# 'RGB' and 'colors', and some special attributes like blink, underline et al., but since they
213+
# are not used by gambaterm, it is not called unless we find better reason otherwise.
214+
# terminal.probe_xtgettcap(timeout=1.0)
215+
216+
# In practice kitty keyboard protocol pretty well implies 24-bit color support already,
211217
color_mode = app_config.color_mode or ColorMode.HAS_24_BIT_COLOR
212218

213219
print(
214-
f"[Terminal Info] {username}: {terminal_type}, {input_source}, {terminal.width}x{terminal.height}"
220+
f"[Terminal Info] {username}: term={terminal_type}, {input_source}, {terminal.width}x{terminal.height}"
215221
)
216222

217223
try:

gambaterm/ssh_app_session.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,19 @@ async def process_to_terminal(
7777
process: SSHServerProcess[str],
7878
executor: ThreadPoolExecutor,
7979
target: Callable[[RemoteTerminal], T],
80+
terminal_type: str | None = None,
8081
) -> T:
8182
"""Create a blessed RemoteTerminal from an SSH process.
8283
8384
Once the redirections are set up, I/O become synchronous,
8485
so we run the target function in a thread executor to avoid blocking the event loop
86+
87+
:param process: SSHServerProcess string
88+
:param executor: ThreadPoolExecutor for running the game thread
89+
:param target: callable receiving the RemoteTerminal, run in executor
90+
:param terminal_type: terminal type from SSH SendEnv/AcceptEnv, `TERM` value
91+
:returns: return value of *target*
92+
8593
"""
8694
width, height, _, _ = process.get_terminal_size()
8795
if width == height == 0:
@@ -94,6 +102,7 @@ def _target() -> T:
94102
keyboard_fd=keyboard_fd,
95103
rows=height,
96104
columns=width,
105+
kind=terminal_type,
97106
)
98107
with _bind_resize(process, ssh_term):
99108
return target(ssh_term)

gambaterm/telnet.py

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,7 @@
3333
is_kitty_keyboard_protocol_supported,
3434
)
3535
from .remote_terminal import RemoteTerminal
36-
from .telnet_app_session import (
37-
telnet_to_terminal,
38-
)
36+
from .telnet_app_session import telnet_to_terminal
3937

4038

4139
def _save_dir_name(username: str | None) -> str:
@@ -53,7 +51,6 @@ def thread_target(
5351
terminal: RemoteTerminal,
5452
console_callback: Callable[[], Console],
5553
app_config: AppConfig,
56-
color_mode: ColorMode,
5754
username: str | None,
5855
) -> int:
5956
"""Run the emulator in a thread with the given RemoteTerminal."""
@@ -76,6 +73,14 @@ def thread_target(
7673
print(f"< User `{username}` did not support keyboard protocol")
7774
return 1
7875

76+
# It is possible, here, to probe XTGETTCAP which helps correct terminal.number_of_colors using
77+
# 'RGB' and 'colors', and some special attributes like blink, underline et al., but since they
78+
# are not used by gambaterm, it is not called unless we find better reason otherwise.
79+
# terminal.probe_xtgettcap(timeout=1.0)
80+
81+
# In practice kitty keyboard protocol pretty well implies 24-bit color support already,
82+
color_mode = app_config.color_mode or ColorMode.HAS_24_BIT_COLOR
83+
7984
try:
8085
terminal.stream.write(
8186
terminal.enter_fullscreen + terminal.clear + terminal.hide_cursor
@@ -90,6 +95,7 @@ def thread_target(
9095
color_mode=color_mode,
9196
break_after=app_config.break_after,
9297
speed=app_config.speed,
98+
use_cpr_sync=app_config.cpr_sync,
9399
)
94100
except (KeyboardInterrupt, EOFError):
95101
return 0
@@ -244,20 +250,13 @@ async def _telnet_shell(
244250
except (asyncio.TimeoutError, KeyError):
245251
pass
246252

247-
terminal_type = writer.get_extra_info("TERM") or "unknown"
253+
terminal_type = writer.get_extra_info("TERM") or None
248254
username = writer.get_extra_info("USER") or None
249255
print(
250-
f"> Telnet client connected ({peer_host}:{peer_port})"
256+
f"> Telnet client connected ({peer_host}:{peer_port} term={terminal_type})"
251257
+ (f" user={username}" if username else "")
252258
)
253259

254-
if terminal_type == "unknown":
255-
print("Warning: terminal type not negotiated, assuming xterm-256color.")
256-
terminal_type = "xterm-256color"
257-
258-
# Kitty keyboard protocol implies 24-bit color support
259-
color_mode = app_config.color_mode or ColorMode.HAS_24_BIT_COLOR
260-
261260
if idle_timeout is not None:
262261
stats_task = asyncio.create_task(
263262
_log_connection_stats(reader, writer, peer_host, peer_port, idle_timeout)
@@ -267,10 +266,7 @@ async def _telnet_shell(
267266

268267
cols = writer.get_extra_info("cols") or 80
269268
rows = writer.get_extra_info("rows") or 24
270-
print(
271-
f"[Terminal Info] {peer_host}: {terminal_type}, "
272-
f"{color_mode.name}, {cols}x{rows}"
273-
)
269+
print(f"[Terminal Info] {peer_host}: ttype={terminal_type}, {cols}x{rows}")
274270

275271
try:
276272
# Copy namespace and set telnet-specific save directory
@@ -289,13 +285,14 @@ async def _telnet_shell(
289285
config = AppConfig(**vars(namespace))
290286

291287
def target(term: RemoteTerminal) -> int:
292-
return thread_target(term, console_callback, config, color_mode, username)
288+
return thread_target(term, console_callback, config, username)
293289

294290
return await telnet_to_terminal(
295291
reader,
296292
writer,
297293
executor,
298294
target,
295+
terminal_type=terminal_type,
299296
)
300297
finally:
301298
if stats_task is not None:

gambaterm/telnet_app_session.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,15 +103,17 @@ async def telnet_to_terminal(
103103
writer: TelnetWriter,
104104
executor: ThreadPoolExecutor,
105105
target: Callable[[RemoteTerminal], T],
106+
terminal_type: str | None = None,
106107
) -> T:
107108
"""Create a RemoteTerminal and run *target* in a thread executor.
108109
109-
Sets up a pipe for output forwarding with paced delivery at 60 pps.
110+
Sets up a pipe for i/o forwarding
110111
111112
:param writer: telnetlib3 writer
112113
:param executor: ThreadPoolExecutor for running the game thread
113114
:param target: callable receiving the RemoteTerminal, run in executor
114115
:param input_read_fd: read end of the input pipe (keyboard_fd for blessed)
116+
:param terminal_type: negotiated terminal type from telnet TTYPE or NEW-ENVIRON TERM
115117
:returns: return value of *target*
116118
"""
117119
cols = writer.get_extra_info("cols") or 80
@@ -128,15 +130,16 @@ async def telnet_to_terminal(
128130

129131
def _target() -> T:
130132
try:
131-
# The output stream is created so that the remote terminal can be instanciated.
132-
# However, it's not used in practice since the `run` function writes bytes directly to the file decriptor.
133+
# The output stream is created so that the remote terminal can be instantiated.
134+
# However, it's not used in practice since the `run` function writes bytes directly to the file descriptor.
133135
# Still, this context manager is responsible for closing the `output_write_fd` file descriptor.
134136
with open(output_write_fd, "w", newline="\r\n") as stream:
135137
telnet_term = RemoteTerminal(
136138
stream=stream,
137139
keyboard_fd=input_read_fd,
138140
rows=rows,
139141
columns=cols,
142+
kind=terminal_type,
140143
)
141144
with bind_resize_telnet(writer, telnet_term):
142145
return target(telnet_term)

0 commit comments

Comments
 (0)