1-
1+ # Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file.
22import re
33import sys
4+ import os
45
56from .ansi import AnsiFore , AnsiBack , AnsiStyle , Style
67from .winterm import WinTerm , WinColor , WinStyle
7- from .win32 import windll
8+ from .win32 import windll , winapi_test
9+
810
911winterm = None
1012if windll is not None :
1113 winterm = WinTerm ()
1214
1315
16+ def is_stream_closed (stream ):
17+ return not hasattr (stream , 'closed' ) or stream .closed
18+
19+
1420def is_a_tty (stream ):
1521 return hasattr (stream , 'isatty' ) and stream .isatty ()
1622
@@ -40,7 +46,8 @@ class AnsiToWin32(object):
4046 sequences from the text, and if outputting to a tty, will convert them into
4147 win32 function calls.
4248 '''
43- ANSI_RE = re .compile ('\033 \[((?:\d|;)*)([a-zA-Z])' )
49+ ANSI_CSI_RE = re .compile ('\001 ?\033 \[((?:\d|;)*)([a-zA-Z])\002 ?' ) # Control Sequence Introducer
50+ ANSI_OSC_RE = re .compile ('\001 ?\033 \]((?:.|;)*?)(\x07 )\002 ?' ) # Operating System Command
4451
4552 def __init__ (self , wrapped , convert = None , strip = None , autoreset = False ):
4653 # The wrapped stream (normally sys.stdout or sys.stderr)
@@ -52,16 +59,21 @@ def __init__(self, wrapped, convert=None, strip=None, autoreset=False):
5259 # create the proxy wrapping our output stream
5360 self .stream = StreamWrapper (wrapped , self )
5461
55- on_windows = sys .platform .startswith ('win' )
62+ on_windows = os .name == 'nt'
63+ # We test if the WinAPI works, because even if we are on Windows
64+ # we may be using a terminal that doesn't support the WinAPI
65+ # (e.g. Cygwin Terminal). In this case it's up to the terminal
66+ # to support the ANSI codes.
67+ conversion_supported = on_windows and winapi_test ()
5668
5769 # should we strip ANSI sequences from our output?
5870 if strip is None :
59- strip = on_windows
71+ strip = conversion_supported or ( not is_stream_closed ( wrapped ) and not is_a_tty ( wrapped ))
6072 self .strip = strip
6173
6274 # should we should convert ANSI sequences into win32 calls?
6375 if convert is None :
64- convert = on_windows and is_a_tty (wrapped )
76+ convert = conversion_supported and not is_stream_closed ( wrapped ) and is_a_tty (wrapped )
6577 self .convert = convert
6678
6779 # dict of ansi codes to win32 functions and parameters
@@ -70,7 +82,6 @@ def __init__(self, wrapped, convert=None, strip=None, autoreset=False):
7082 # are we wrapping stderr?
7183 self .on_stderr = self .wrapped is sys .stderr
7284
73-
7485 def should_wrap (self ):
7586 '''
7687 True if this class is actually needed. If false, then the output
@@ -81,7 +92,6 @@ def should_wrap(self):
8192 '''
8293 return self .convert or self .strip or self .autoreset
8394
84-
8595 def get_win32_calls (self ):
8696 if self .convert and winterm :
8797 return {
@@ -98,6 +108,14 @@ def get_win32_calls(self):
98108 AnsiFore .CYAN : (winterm .fore , WinColor .CYAN ),
99109 AnsiFore .WHITE : (winterm .fore , WinColor .GREY ),
100110 AnsiFore .RESET : (winterm .fore , ),
111+ AnsiFore .LIGHTBLACK_EX : (winterm .fore , WinColor .BLACK , True ),
112+ AnsiFore .LIGHTRED_EX : (winterm .fore , WinColor .RED , True ),
113+ AnsiFore .LIGHTGREEN_EX : (winterm .fore , WinColor .GREEN , True ),
114+ AnsiFore .LIGHTYELLOW_EX : (winterm .fore , WinColor .YELLOW , True ),
115+ AnsiFore .LIGHTBLUE_EX : (winterm .fore , WinColor .BLUE , True ),
116+ AnsiFore .LIGHTMAGENTA_EX : (winterm .fore , WinColor .MAGENTA , True ),
117+ AnsiFore .LIGHTCYAN_EX : (winterm .fore , WinColor .CYAN , True ),
118+ AnsiFore .LIGHTWHITE_EX : (winterm .fore , WinColor .GREY , True ),
101119 AnsiBack .BLACK : (winterm .back , WinColor .BLACK ),
102120 AnsiBack .RED : (winterm .back , WinColor .RED ),
103121 AnsiBack .GREEN : (winterm .back , WinColor .GREEN ),
@@ -107,8 +125,16 @@ def get_win32_calls(self):
107125 AnsiBack .CYAN : (winterm .back , WinColor .CYAN ),
108126 AnsiBack .WHITE : (winterm .back , WinColor .GREY ),
109127 AnsiBack .RESET : (winterm .back , ),
128+ AnsiBack .LIGHTBLACK_EX : (winterm .back , WinColor .BLACK , True ),
129+ AnsiBack .LIGHTRED_EX : (winterm .back , WinColor .RED , True ),
130+ AnsiBack .LIGHTGREEN_EX : (winterm .back , WinColor .GREEN , True ),
131+ AnsiBack .LIGHTYELLOW_EX : (winterm .back , WinColor .YELLOW , True ),
132+ AnsiBack .LIGHTBLUE_EX : (winterm .back , WinColor .BLUE , True ),
133+ AnsiBack .LIGHTMAGENTA_EX : (winterm .back , WinColor .MAGENTA , True ),
134+ AnsiBack .LIGHTCYAN_EX : (winterm .back , WinColor .CYAN , True ),
135+ AnsiBack .LIGHTWHITE_EX : (winterm .back , WinColor .GREY , True ),
110136 }
111-
137+ return dict ()
112138
113139 def write (self , text ):
114140 if self .strip or self .convert :
@@ -123,7 +149,7 @@ def write(self, text):
123149 def reset_all (self ):
124150 if self .convert :
125151 self .call_win32 ('m' , (0 ,))
126- elif is_a_tty (self .wrapped ):
152+ elif not self . strip and not is_stream_closed (self .wrapped ):
127153 self .wrapped .write (Style .RESET_ALL )
128154
129155
@@ -134,7 +160,8 @@ def write_and_convert(self, text):
134160 calls.
135161 '''
136162 cursor = 0
137- for match in self .ANSI_RE .finditer (text ):
163+ text = self .convert_osc (text )
164+ for match in self .ANSI_CSI_RE .finditer (text ):
138165 start , end = match .span ()
139166 self .write_plain_text (text , cursor , start )
140167 self .convert_ansi (* match .groups ())
@@ -150,21 +177,29 @@ def write_plain_text(self, text, start, end):
150177
151178 def convert_ansi (self , paramstring , command ):
152179 if self .convert :
153- params = self .extract_params (paramstring )
180+ params = self .extract_params (command , paramstring )
154181 self .call_win32 (command , params )
155182
156183
157- def extract_params (self , paramstring ):
158- def split (paramstring ):
159- for p in paramstring .split (';' ):
160- if p != '' :
161- yield int (p )
162- return tuple (split (paramstring ))
184+ def extract_params (self , command , paramstring ):
185+ if command in 'Hf' :
186+ params = tuple (int (p ) if len (p ) != 0 else 1 for p in paramstring .split (';' ))
187+ while len (params ) < 2 :
188+ # defaults:
189+ params = params + (1 ,)
190+ else :
191+ params = tuple (int (p ) for p in paramstring .split (';' ) if len (p ) != 0 )
192+ if len (params ) == 0 :
193+ # defaults:
194+ if command in 'JKm' :
195+ params = (0 ,)
196+ elif command in 'ABCD' :
197+ params = (1 ,)
198+
199+ return params
163200
164201
165202 def call_win32 (self , command , params ):
166- if params == []:
167- params = [0 ]
168203 if command == 'm' :
169204 for param in params :
170205 if param in self .win32_calls :
@@ -173,17 +208,29 @@ def call_win32(self, command, params):
173208 args = func_args [1 :]
174209 kwargs = dict (on_stderr = self .on_stderr )
175210 func (* args , ** kwargs )
176- elif command in ('H' , 'f' ): # set cursor position
177- func = winterm .set_cursor_position
178- func (params , on_stderr = self .on_stderr )
179- elif command in ('J' ):
180- func = winterm .erase_data
181- func (params , on_stderr = self .on_stderr )
182- elif command == 'A' :
183- if params == () or params == None :
184- num_rows = 1
185- else :
186- num_rows = params [0 ]
187- func = winterm .cursor_up
188- func (num_rows , on_stderr = self .on_stderr )
189-
211+ elif command in 'J' :
212+ winterm .erase_screen (params [0 ], on_stderr = self .on_stderr )
213+ elif command in 'K' :
214+ winterm .erase_line (params [0 ], on_stderr = self .on_stderr )
215+ elif command in 'Hf' : # cursor position - absolute
216+ winterm .set_cursor_position (params , on_stderr = self .on_stderr )
217+ elif command in 'ABCD' : # cursor position - relative
218+ n = params [0 ]
219+ # A - up, B - down, C - forward, D - back
220+ x , y = {'A' : (0 , - n ), 'B' : (0 , n ), 'C' : (n , 0 ), 'D' : (- n , 0 )}[command ]
221+ winterm .cursor_adjust (x , y , on_stderr = self .on_stderr )
222+
223+
224+ def convert_osc (self , text ):
225+ for match in self .ANSI_OSC_RE .finditer (text ):
226+ start , end = match .span ()
227+ text = text [:start ] + text [end :]
228+ paramstring , command = match .groups ()
229+ if command in '\x07 ' : # \x07 = BEL
230+ params = paramstring .split (";" )
231+ # 0 - change title and icon (we will only change title)
232+ # 1 - change icon (we don't support this)
233+ # 2 - change title
234+ if params [0 ] in '02' :
235+ winterm .set_title (params [1 ])
236+ return text
0 commit comments