11"""
2- Provide an async context manager to create a blessed SSHTerminal
2+ Provide an async context manager to create a blessed RemoteTerminal
33from an AsyncSSH process.
44"""
55from __future__ import annotations
66
77import os
8- import codecs
98import asyncio
10- import contextlib
119import subprocess
1210from concurrent .futures import ThreadPoolExecutor
1311from contextlib import asynccontextmanager , contextmanager
14- from typing import AsyncIterator , Generator , IO , Iterator , TypeVar , Callable
15-
16- from blessed import Terminal as BlessedTerminal
17- from blessed .terminal import WINSZ
12+ from typing import AsyncIterator , Iterator , TypeVar , Callable
1813
1914from asyncssh import SSHServerProcess
2015
21- T = TypeVar ("T" )
22-
23-
24- # Python's curses.setupterm() can only be called once per process — subsequent
25- # calls with a different terminal type are silently ignored. Since the SSH
26- # server handles multiple concurrent connections in threads, all SSHTerminal
27- # instances share whatever terminal type was initialized first by the local
28- # Terminal(). We hardcode 'xterm-256color' as the kind since:
29- # 1. It's universally compatible with modern terminals
30- # 2. We use standard VT100/ANSI escape codes directly, not terminfo caps
31- # 3. It avoids issues where the first SSH client's TERM value differs
32- SSH_TERMINAL_TYPE = "xterm-256color"
33-
34-
35- class SSHTerminal (BlessedTerminal ):
36- """A blessed Terminal subclass for SSH streams.
37-
38- Following the pattern from x84 (x84/terminal.py), this stubs raw/cbreak
39- mode (SSH is already raw) and overrides size detection to use values
40- provided by the SSH server.
41- """
16+ from .remote_terminal import RemoteTerminal
4217
43- def __init__ (
44- self ,
45- stream : IO [str ],
46- keyboard_fd : int ,
47- rows : int ,
48- columns : int ,
49- ) -> None :
50- self ._rows = rows
51- self ._columns = columns
52- super ().__init__ (kind = SSH_TERMINAL_TYPE , stream = stream , force_styling = True )
53- # Blessed only sets _keyboard_fd when stream is sys.__stdout__, so
54- # for SSH pipes we must set it and initialize the decoder manually
55- self ._keyboard_fd = keyboard_fd # type: ignore[assignment]
56- self ._keyboard_decoder = codecs .getincrementaldecoder ("UTF-8" )()
57-
58- @property
59- def is_a_tty (self ) -> bool :
60- return True
61-
62- @contextlib .contextmanager
63- def raw (self ) -> Generator [None , None , None ]:
64- yield
65-
66- @contextlib .contextmanager
67- def cbreak (self ) -> Generator [None , None , None ]:
68- yield
69-
70- def _height_and_width (self ) -> WINSZ :
71- return WINSZ (
72- ws_row = self ._rows ,
73- ws_col = self ._columns ,
74- ws_xpixel = 0 ,
75- ws_ypixel = 0 ,
76- )
77-
78- def update_size (self , rows : int , columns : int ) -> None :
79- self ._rows = rows
80- self ._columns = columns
18+ T = TypeVar ("T" )
8119
8220
8321@asynccontextmanager
@@ -131,7 +69,7 @@ async def _input_pipe_from_process(
13169
13270@contextmanager
13371def _bind_resize (
134- process : SSHServerProcess [str ], ssh_term : SSHTerminal
72+ process : SSHServerProcess [str ], ssh_term : RemoteTerminal
13573) -> Iterator [None ]:
13674 original_method = process .terminal_size_changed
13775
@@ -151,9 +89,9 @@ def terminal_size_changed(
15189async def process_to_terminal (
15290 process : SSHServerProcess [str ],
15391 executor : ThreadPoolExecutor ,
154- target : Callable [[SSHTerminal ], T ],
92+ target : Callable [[RemoteTerminal ], T ],
15593) -> T :
156- """Create a blessed SSHTerminal from an SSH process.
94+ """Create a blessed RemoteTerminal from an SSH process.
15795
15896 Once the redirections are set up, I/O become synchronous,
15997 so we run the target function in a thread executor to avoid blocking the event loop
@@ -164,7 +102,7 @@ async def process_to_terminal(
164102
165103 def _target () -> T :
166104 with open (write_fd , "w" , newline = "\r \n " ) as stream :
167- ssh_term = SSHTerminal (
105+ ssh_term = RemoteTerminal (
168106 stream = stream ,
169107 keyboard_fd = keyboard_fd ,
170108 rows = height ,
0 commit comments