1+ from itertools import count
12import sys
23from enum import Enum , auto
34from dataclasses import dataclass
@@ -40,8 +41,8 @@ class OSC:
4041
4142 CHAR = "]"
4243 BELL = "\x07 "
43- ST1 = "\x5c "
44- ST2 = "\x9c "
44+ ESC_ST = "\x5c "
45+ SINGLE_ST = "\x9c "
4546
4647 def raw (self ) -> str :
4748 return f"\033 { self .CHAR } { self .payload } { self .BELL } "
@@ -52,77 +53,126 @@ class DCS:
5253 payload : str
5354
5455 CHAR = "P"
55- ST1 = "\x5c "
56- ST2 = "\x9c "
56+ ESC_ST = "\x5c "
57+ SINGLE_ST = "\x9c "
5758
5859 def raw (self ) -> str :
59- return f"\033 { self .CHAR } { self .payload } \033 { self .ST1 } "
60+ return f"\033 { self .CHAR } { self .payload } \033 { self .ESC_ST } "
6061
6162
6263EscapeCode : TypeAlias = CSI | OSC | DCS
64+ Ready : TypeAlias = list [EscapeCode | str ]
6365
6466
65- def parse_ansi_escape_code () -> Generator [EscapeCode | str | None , str , None ]:
66- ready : EscapeCode | str | None = None
67+ def parse_ansi_escape_code () -> Generator [Ready , str , None ]:
68+ # Initialize result list
69+ ready : Ready = []
70+
71+ # Get first data
72+ data = yield ready
73+
74+ # Loop over ansi escape code
6775 while True :
68- char = yield ready
69- while char != "\033 " :
70- char = yield char
71- code = yield None
76+ # Wait for escape
77+ while "\033 " not in data :
78+ ready .append (data )
79+ data = yield ready
80+ ready = []
81+
82+ # Split the data
83+ before , data = data .split ("\033 " , maxsplit = 1 )
84+ ready .append (before )
85+
86+ # Wait for new data if necessary
87+ while not data :
88+ data = yield ready
89+ ready = []
90+
91+ # Match code
92+ code = data [0 ]
7293 if code == CSI .CHAR :
73- ready = yield from parse_csi ()
94+ data , ready = yield from parse_csi (data , ready )
7495 elif code == OSC .CHAR :
75- ready = yield from parse_osc ()
96+ data , ready = yield from parse_osc (data , ready )
7697 elif code == DCS .CHAR :
77- ready = yield from parse_dcs ()
98+ data , ready = yield from parse_dcs (data , ready )
7899 else :
79- ready = char + code
100+ ready . append ( " \033 " )
80101
81102
82- def parse_csi () -> Generator [None , str , CSI ]:
83- payload = ""
84- while True :
85- char = yield None
103+ def parse_csi (
104+ data : str , ready : list [EscapeCode | str ]
105+ ) -> Generator [Ready , str , tuple [str , Ready ]]:
106+ # Loop over characters
107+ for i in count (1 ):
108+ # Get more data if necessary
109+ if i == len (data ):
110+ data += yield ready
111+ ready = []
112+ char = data [i ]
113+
114+ # Check for stop code
86115 if char in CSI .CODES :
87- break
88- payload += char
89- return CSI (char , payload )
116+ ready .append (CSI (char , data [1 :i ]))
117+ return data [i + 1 :], ready
90118
119+ assert False
91120
92- def parse_osc () -> Generator [None , str , OSC ]:
93- payload = ""
94- while True :
95- char = yield None
96- while char == "\033 " :
97- extra = yield None
98- if extra == OSC .ST1 :
99- return OSC (payload )
100- payload += char
101- char = extra
102- if char in (OSC .BELL , OSC .ST2 ):
103- return OSC (payload )
104- payload += char
105-
106-
107- def parse_dcs () -> Generator [None , str , DCS ]:
108- payload = ""
109- while True :
110- char = yield None
111- while char == "\033 " :
112- extra = yield None
113- if extra == OSC .ST1 :
114- return DCS (payload )
115- payload += char
116- char = extra
117- if char == OSC .ST2 :
118- return DCS (payload )
119- payload += char
121+
122+ def parse_osc (
123+ data : str , ready : list [EscapeCode | str ]
124+ ) -> Generator [Ready , str , tuple [str , Ready ]]:
125+ # Loop over characters
126+ for i in count (1 ):
127+ # Get more data if necessary
128+ if i == len (data ):
129+ data += yield ready
130+ ready = []
131+ before = data [i - 1 ]
132+ char = data [i ]
133+
134+ # Check for double character stop code
135+ if before == "\033 " and char == OSC .ESC_ST :
136+ ready .append (OSC (data [1 : i - 1 ]))
137+ return data [i + 1 :], ready
138+
139+ # Check for single character stop code
140+ if char in (OSC .BELL , OSC .SINGLE_ST ):
141+ ready .append (OSC (data [1 :i ]))
142+ return data [i + 1 :], ready
143+
144+ assert False
145+
146+
147+ def parse_dcs (
148+ data : str , ready : list [EscapeCode | str ]
149+ ) -> Generator [Ready , str , tuple [str , Ready ]]:
150+ # Loop over characters
151+ for i in count (1 ):
152+ # Get more data if necessary
153+ if i == len (data ):
154+ data += yield ready
155+ ready = []
156+ before = data [i - 1 ]
157+ char = data [i ]
158+
159+ # Check for double character stop code
160+ if before == "\033 " and char == DCS .ESC_ST :
161+ ready .append (DCS (data [1 : i - 1 ]))
162+ return data [i + 1 :], ready
163+
164+ # Check for single character stop code
165+ if char == DCS .SINGLE_ST :
166+ ready .append (DCS (data [1 :i ]))
167+ return data [i + 1 :], ready
168+
169+ assert False
120170
121171
122172def detect_true_color_support_with_DCS () -> Generator [str | None , str , SupportStatus ]:
123173 """
124- This detection is restrictive, i.e it has not known positive
125- However, it falsely reports no support for gnome terminal
174+ This detection is restrictive, i.e it has no known false positive.
175+ However, it falsely reports no support for gnome- terminal
126176 """
127177 # Set unlikely RGB value
128178 command = "\033 [48;2;1;2;3m"
@@ -133,21 +183,22 @@ def detect_true_color_support_with_DCS() -> Generator[str | None, str, SupportSt
133183 # Query primary device attributes
134184 command += "\033 [c"
135185 # Send command
136- char = yield command
186+ data = yield command
137187 # Prepare coroutine
138188 coro = parse_ansi_escape_code ()
139- assert next (coro ) is None
189+ assert not next (coro )
140190 # Loop over characters
141191 result = SupportStatus .Undecided
142192 while True :
143- item = coro .send (char )
144- if isinstance (item , CSI ):
145- if item .code == "c" :
146- return result
147- if isinstance (item , DCS ):
148- if item .payload .endswith (":1:2:3m" ):
149- result = SupportStatus .Supported
150- char = yield None
193+ ready = coro .send (data )
194+ for item in ready :
195+ if isinstance (item , CSI ):
196+ if item .code == "c" :
197+ return result
198+ if isinstance (item , DCS ):
199+ if item .payload .endswith (":1:2:3m" ):
200+ result = SupportStatus .Supported
201+ data = yield None
151202
152203
153204def detect_true_color_support_with_OSC () -> Generator [str | None , str , SupportStatus ]:
@@ -160,21 +211,22 @@ def detect_true_color_support_with_OSC() -> Generator[str | None, str, SupportSt
160211 # Query primary device attributes
161212 command += "\033 [c"
162213 # Send command
163- char = yield command
214+ data = yield command
164215 # Prepare coroutine
165216 coro = parse_ansi_escape_code ()
166- assert next (coro ) is None
217+ assert not next (coro )
167218 # Loop over characters
168219 result = SupportStatus .Unsupported
169220 while True :
170- item = coro .send (char )
171- if isinstance (item , CSI ):
172- if item .code == "c" :
173- return result
174- if isinstance (item , OSC ):
175- if "rgb" in item .payload :
176- result = SupportStatus .Undecided
177- char = yield None
221+ ready = coro .send (data )
222+ for item in ready :
223+ if isinstance (item , CSI ):
224+ if item .code == "c" :
225+ return result
226+ if isinstance (item , OSC ):
227+ if "rgb" in item .payload :
228+ result = SupportStatus .Undecided
229+ data = yield None
178230
179231
180232def detect_true_color_support_parser () -> Generator [str | None , str , SupportStatus ]:
@@ -195,20 +247,21 @@ def detect_keyboard_protocol_support_parser() -> (
195247 # Query primary device attributes
196248 command += "\033 [c"
197249 # Send command
198- char = yield command
250+ data = yield command
199251 # Prepare coroutine
200252 coro = parse_ansi_escape_code ()
201- assert next (coro ) is None
253+ assert not next (coro )
202254 # Loop over characters
203255 result = SupportStatus .Unsupported
204256 while True :
205- item = coro .send (char )
206- if isinstance (item , CSI ):
207- if item .code == "c" :
208- return result
209- if item .code == "u" :
210- result = SupportStatus .Supported
211- char = yield None
257+ ready = coro .send (data )
258+ for item in ready :
259+ if isinstance (item , CSI ):
260+ if item .code == "c" :
261+ return result
262+ if item .code == "u" :
263+ result = SupportStatus .Supported
264+ data = yield None
212265
213266
214267def run_parser_in_app_session (
@@ -229,13 +282,12 @@ def run_parser_in_app_session(
229282 raise KeyboardInterrupt
230283
231284 # Send each char of the key into the coroutine
232- for char in key .data :
233- command = coro .send (char )
285+ command = coro .send (key .data )
234286
235- # Send extra command
236- if command is not None :
237- app_session .output .write_raw (command )
238- app_session .output .flush ()
287+ # Send extra command
288+ if command is not None :
289+ app_session .output .write_raw (command )
290+ app_session .output .flush ()
239291
240292 # Get the result
241293 except StopIteration as exc :
0 commit comments