• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

spyder-ide / qtconsole / 23521956902

25 Mar 2026 02:26AM UTC coverage: 61.505%. Remained the same
23521956902

push

github

ccordoba12
Back to work

1 of 1 new or added line in 1 file covered. (100.0%)

3 existing lines in 1 file now uncovered.

2943 of 4785 relevant lines covered (61.5%)

1.84 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.95
/qtconsole/ansi_code_processor.py
1
""" Utilities for processing ANSI escape codes and special ASCII characters.
2
"""
3
#-----------------------------------------------------------------------------
4
# Imports
5
#-----------------------------------------------------------------------------
6

7
# Standard library imports
8
from collections import namedtuple
3✔
9
import re
3✔
10

11
# System library imports
12
from qtpy import QtGui
3✔
13

14
# Local imports
15
from qtconsole.styles import dark_style
3✔
16

17
#-----------------------------------------------------------------------------
18
# Constants and datatypes
19
#-----------------------------------------------------------------------------
20

21
# An action for cursor visibility requests
22
CursorVisibilityAction = namedtuple(
3✔
23
    'CursorVisibilityAction', ['action', 'visible']
24
)
25

26
# An action for erase requests (ED and EL commands).
27
EraseAction = namedtuple('EraseAction', ['action', 'area', 'erase_to'])
3✔
28

29
# An action for cursor move requests (CUU, CUD, CUF, CUB, CNL, CPL, CHA, CUP,
30
# and HVP commands).
31
# FIXME: Not implemented in AnsiCodeProcessor.
32
MoveAction = namedtuple('MoveAction', ['action', 'dir', 'unit', 'count'])
3✔
33

34
# An action for scroll requests (SU and ST) and form feeds.
35
ScrollAction = namedtuple('ScrollAction', ['action', 'dir', 'unit', 'count'])
3✔
36

37
# An action for the carriage return character
38
CarriageReturnAction = namedtuple('CarriageReturnAction', ['action'])
3✔
39

40
# An action for the \n character
41
NewLineAction = namedtuple('NewLineAction', ['action'])
3✔
42

43
# An action for the beep character
44
BeepAction = namedtuple('BeepAction', ['action'])
3✔
45

46
# An action for backspace
47
BackSpaceAction = namedtuple('BackSpaceAction', ['action'])
3✔
48

49
# Regular expressions.
50
CSI_COMMANDS = 'ABCDEFGHJKSTfmnsuhl'
3✔
51
CSI_SUBPATTERN = '\\[(.*?)([%s])' % CSI_COMMANDS
3✔
52
OSC_SUBPATTERN = '\\](.*?)[\x07\x1b]'
3✔
53
ANSI_PATTERN = ('\x01?\x1b(%s|%s)\x02?' % \
3✔
54
                (CSI_SUBPATTERN, OSC_SUBPATTERN))
55
ANSI_OR_SPECIAL_PATTERN = re.compile('(\a|\b|\r(?!\n)|\r?\n)|(?:%s)' % ANSI_PATTERN)
3✔
56
SPECIAL_PATTERN = re.compile('([\f])')
3✔
57

58
#-----------------------------------------------------------------------------
59
# Classes
60
#-----------------------------------------------------------------------------
61

62
class AnsiCodeProcessor(object):
3✔
63
    """ Translates special ASCII characters and ANSI escape codes into readable
64
        attributes. It also supports a few non-standard, xterm-specific codes.
65
    """
66

67
    # Whether to increase intensity or set boldness for SGR code 1.
68
    # (Different terminals handle this in different ways.)
69
    bold_text_enabled = True
3✔
70

71
    # We provide an empty default color map because subclasses will likely want
72
    # to use a custom color format.
73
    default_color_map = {}
3✔
74

75
    #---------------------------------------------------------------------------
76
    # AnsiCodeProcessor interface
77
    #---------------------------------------------------------------------------
78

79
    def __init__(self):
3✔
80
        self.actions = []
3✔
81
        self.color_map = self.default_color_map.copy()
3✔
82
        self.reset_sgr()
3✔
83

84
    def reset_sgr(self):
3✔
85
        """ Reset graphics attributs to their default values.
86
        """
87
        self.intensity = 0
3✔
88
        self.italic = False
3✔
89
        self.bold = False
3✔
90
        self.underline = False
3✔
91
        self.foreground_color = None
3✔
92
        self.background_color = None
3✔
93

94
    def split_string(self, string):
3✔
95
        """ Yields substrings for which the same escape code applies.
96
        """
97
        self.actions = []
3✔
98
        start = 0
3✔
99

100
        last_char = None
3✔
101
        string = string[:-1] if last_char is not None else string
3✔
102

103
        for match in ANSI_OR_SPECIAL_PATTERN.finditer(string):
3✔
104
            raw = string[start:match.start()]
3✔
105
            substring = SPECIAL_PATTERN.sub(self._replace_special, raw)
3✔
106
            if substring or self.actions:
3✔
107
                yield substring
3✔
108
                self.actions = []
3✔
109
            start = match.end()
3✔
110

111
            groups = [g for g in match.groups() if (g is not None)]
3✔
112
            g0 = groups[0]
3✔
113
            if g0 == '\a':
3✔
114
                self.actions.append(BeepAction('beep'))
3✔
115
                yield None
3✔
116
                self.actions = []
3✔
117
            elif g0 == '\r':
3✔
118
                self.actions.append(CarriageReturnAction('carriage-return'))
3✔
119
                yield None
3✔
120
                self.actions = []
3✔
121
            elif g0 == '\b':
3✔
122
                self.actions.append(BackSpaceAction('backspace'))
3✔
123
                yield None
3✔
124
                self.actions = []
3✔
125
            elif g0 == '\n' or g0 == '\r\n':
3✔
126
                self.actions.append(NewLineAction('newline'))
3✔
127
                yield None
3✔
128
                self.actions = []
3✔
129
            else:
130
                params = []
3✔
131
                if g0.startswith('['):
3✔
132
                    raw_params = groups[1] or ""
3✔
133

134
                    # Handle private mode sequences
135
                    if raw_params.startswith('?'):
3✔
136
                        raw_params = raw_params[1:]
3✔
137

138
                    params = [p for p in raw_params.split(';') if p]
3✔
139

140
                    try:
3✔
141
                        params = list(map(int, params))
3✔
142
                    except ValueError:
×
143
                        # Silently discard badly formed codes.
144
                        pass
×
145
                    else:
146
                        self.set_csi_code(groups[2], params)
3✔
147

148
                elif g0.startswith(']'):
3✔
149
                    # Case 2: OSC code.
150
                    params = [param for param in groups[1].split(';') if param]
3✔
151
                    self.set_osc_code(params)
3✔
152

153
        raw = string[start:]
3✔
154
        substring = SPECIAL_PATTERN.sub(self._replace_special, raw)
3✔
155
        if substring or self.actions:
3✔
156
            yield substring
3✔
157

158
        if last_char is not None:
3✔
159
            self.actions.append(NewLineAction('newline'))
×
160
            yield None
×
161

162
    def set_csi_code(self, command, params=[]):
3✔
163
        """ Set attributes based on CSI (Control Sequence Introducer) code.
164

165
        Parameters
166
        ----------
167
        command : str
168
            The code identifier, i.e. the final character in the sequence.
169

170
        params : sequence of integers, optional
171
            The parameter codes for the command.
172
        """
173

174
        if command in ('h', 'l'):
3✔
175
            if params == [25]:
3✔
176
                visible = (command == 'h')
3✔
177
                self.actions.append(
3✔
178
                    CursorVisibilityAction('cursor-visibility', visible)
179
                )
180
        if command == 'm':   # SGR - Select Graphic Rendition
3✔
181
            if params:
3✔
182
                self.set_sgr_code(params)
3✔
183
            else:
184
                self.set_sgr_code([0])
×
185

186
        elif (command == 'J' or # ED - Erase Data
3✔
187
              command == 'K'):  # EL - Erase in Line
188
            code = params[0] if params else 0
3✔
189
            if 0 <= code <= 2:
3✔
190
                area = 'screen' if command == 'J' else 'line'
3✔
191
                if code == 0:
3✔
192
                    erase_to = 'end'
3✔
193
                elif code == 1:
3✔
194
                    erase_to = 'start'
3✔
195
                elif code == 2:
3✔
196
                    erase_to = 'all'
3✔
197
                self.actions.append(EraseAction('erase', area, erase_to))
3✔
198

199
        elif (command == 'S' or # SU - Scroll Up
3✔
200
              command == 'T'):  # SD - Scroll Down
201
            dir = 'up' if command == 'S' else 'down'
3✔
202
            count = params[0] if params else 1
3✔
203
            self.actions.append(ScrollAction('scroll', dir, 'line', count))
3✔
204

205
        elif command == 'A':  # Move N lines Up
3✔
206
            dir = 'up'
3✔
207
            count = params[0] if params else 1
3✔
208
            self.actions.append(MoveAction('move', dir, 'line', count))
3✔
209

210
        elif command == 'B':  # Move N lines Down
3✔
211
            dir = 'down'
×
212
            count = params[0] if params else 1
×
213
            self.actions.append(MoveAction('move', dir, 'line', count))
×
214

215
        elif command == 'F':  # Goes back to the begining of the n-th previous line
3✔
216
            dir = 'leftup'
3✔
217
            count = params[0] if params else 1
3✔
218
            self.actions.append(MoveAction('move', dir, 'line', count))
3✔
219
        
220

221
    def set_osc_code(self, params):
3✔
222
        """ Set attributes based on OSC (Operating System Command) parameters.
223

224
        Parameters
225
        ----------
226
        params : sequence of str
227
            The parameters for the command.
228
        """
229
        try:
3✔
230
            command = int(params.pop(0))
3✔
231
        except (IndexError, ValueError):
×
232
            return
×
233

234
        if command == 4:
3✔
235
            # xterm-specific: set color number to color spec.
236
            try:
3✔
237
                color = int(params.pop(0))
3✔
238
                spec = params.pop(0)
3✔
239
                self.color_map[color] = self._parse_xterm_color_spec(spec)
3✔
240
            except (IndexError, ValueError):
×
241
                pass
×
242

243
    def set_sgr_code(self, params):
3✔
244
        """ Set attributes based on SGR (Select Graphic Rendition) codes.
245

246
        Parameters
247
        ----------
248
        params : sequence of ints
249
            A list of SGR codes for one or more SGR commands. Usually this
250
            sequence will have one element per command, although certain
251
            xterm-specific commands requires multiple elements.
252
        """
253
        # Always consume the first parameter.
254
        if not params:
3✔
255
            return
3✔
256
        code = params.pop(0)
3✔
257

258
        if code == 0:
3✔
259
            self.reset_sgr()
3✔
260
        elif code == 1:
3✔
261
            if self.bold_text_enabled:
3✔
262
                self.bold = True
3✔
263
            else:
264
                self.intensity = 1
×
265
        elif code == 2:
3✔
266
            self.intensity = 0
×
267
        elif code == 3:
3✔
UNCOV
268
            self.italic = True
×
269
        elif code == 4:
3✔
270
            self.underline = True
×
271
        elif code == 22:
3✔
272
            self.intensity = 0
×
273
            self.bold = False
×
274
        elif code == 23:
3✔
275
            self.italic = False
×
276
        elif code == 24:
3✔
277
            self.underline = False
×
278
        elif code >= 30 and code <= 37:
3✔
279
            self.foreground_color = code - 30
3✔
280
        elif code == 38 and params:
3✔
281
            _color_type = params.pop(0)
3✔
282
            if _color_type == 5 and params:
3✔
283
                # xterm-specific: 256 color support.
284
                self.foreground_color = params.pop(0)
3✔
285
            elif _color_type == 2:
3✔
286
                # 24bit true colour support.
287
                self.foreground_color = params[:3]
3✔
288
                params[:3] = []
3✔
289
        elif code == 39:
3✔
290
            self.foreground_color = None
3✔
291
        elif code >= 40 and code <= 47:
3✔
292
            self.background_color = code - 40
3✔
293
        elif code == 48 and params:
3✔
294
            _color_type = params.pop(0)
3✔
295
            if _color_type == 5 and params:
3✔
296
                # xterm-specific: 256 color support.
297
                self.background_color = params.pop(0)
3✔
298
            elif _color_type == 2:
3✔
299
                # 24bit true colour support.
300
                self.background_color = params[:3]
3✔
301
                params[:3] = []
3✔
302
        elif code == 49:
3✔
303
            self.background_color = None
3✔
304
        elif code >= 90 and code <= 97:
3✔
305
            # Bright foreground color
306
            self.foreground_color = code - 90
3✔
307
            self.intensity = 1
3✔
308
        elif code >=100 and code <= 107:
×
309
            # Bright background color
310
            self.background_color = code - 100
×
311
            self.intensity = 1
×
312

313
        # Recurse with unconsumed parameters.
314
        self.set_sgr_code(params)
3✔
315

316
    #---------------------------------------------------------------------------
317
    # Protected interface
318
    #---------------------------------------------------------------------------
319

320
    def _parse_xterm_color_spec(self, spec):
3✔
321
        if spec.startswith('rgb:'):
3✔
322
            return tuple(map(lambda x: int(x, 16), spec[4:].split('/')))
3✔
323
        elif spec.startswith('rgbi:'):
3✔
324
            return tuple(map(lambda x: int(float(x) * 255),
3✔
325
                             spec[5:].split('/')))
326
        elif spec == '?':
×
327
            raise ValueError('Unsupported xterm color spec')
×
328
        return spec
×
329

330
    def _replace_special(self, match):
3✔
331
        special = match.group(1)
3✔
332
        if special == '\f':
3✔
333
            self.actions.append(ScrollAction('scroll', 'down', 'page', 1))
3✔
334
        return ''
3✔
335

336
    def _parse_ansi_color(self, color, intensity):
3✔
337
        """
338
        Map an ANSI color code to color name or a RGB tuple.
339
        Based on: https://gist.github.com/MightyPork/1d9bd3a3fd4eb1a661011560f6921b5b
340
        """
341
        parsed_color = None
3✔
342
        if color < 16:
3✔
343
            # Adjust for intensity, if possible.
344
            if intensity > 0 and color < 8:
3✔
345
                color += 8
3✔
346
            parsed_color = self.color_map.get(color, None)
3✔
347
        elif (color > 231):
3✔
UNCOV
348
                s = int((color - 232) * 10 + 8)
×
UNCOV
349
                parsed_color = (s, s, s)
×
350
        else:
351
            n = color - 16
3✔
352
            b = n % 6
3✔
353
            g = (n - b) / 6 % 6
3✔
354
            r = (n - b - g * 6) / 36 % 6
3✔
355
            r = int(r * 40 + 55) if r else 0
3✔
356
            g = int(g * 40 + 55) if g else 0
3✔
357
            b = int(b * 40 + 55) if b else 0
3✔
358
            parsed_color = (r, g, b)
3✔
359
        return parsed_color
3✔
360

361

362
class QtAnsiCodeProcessor(AnsiCodeProcessor):
3✔
363
    """ Translates ANSI escape codes into QTextCharFormats.
364
    """
365

366
    # A map from ANSI color codes to SVG color names or RGB(A) tuples.
367
    darkbg_color_map = {
3✔
368
        0  : 'black',       # black
369
        1  : 'darkred',     # red
370
        2  : 'darkgreen',   # green
371
        3  : 'gold',       # yellow
372
        4  : 'darkblue',    # blue
373
        5  : 'darkviolet',  # magenta
374
        6  : 'steelblue',   # cyan
375
        7  : 'grey',        # white
376
        8  : 'grey',        # black (bright)
377
        9  : 'red',         # red (bright)
378
        10 : 'lime',        # green (bright)
379
        11 : 'yellow',      # yellow (bright)
380
        12 : 'deepskyblue', # blue (bright)
381
        13 : 'magenta',     # magenta (bright)
382
        14 : 'cyan',        # cyan (bright)
383
        15 : 'white' }      # white (bright)
384

385
    # Set the default color map for super class.
386
    default_color_map = darkbg_color_map.copy()
3✔
387

388
    def get_color(self, color, intensity=0):
3✔
389
        """ Returns a QColor for a given color code or rgb list, or None if one
390
            cannot be constructed.
391
        """
392
        if isinstance(color, int):
3✔
393
            constructor = self._parse_ansi_color(color, intensity)
3✔
394
        elif isinstance(color, (tuple, list)):
3✔
395
            constructor = color
×
396
        else:
397
            return None
3✔
398

399
        if isinstance(constructor, str):
3✔
400
            # If this is an X11 color name, we just hope there is a close SVG
401
            # color name. We could use QColor's static method
402
            # 'setAllowX11ColorNames()', but this is global and only available
403
            # on X11. It seems cleaner to aim for uniformity of behavior.
404
            return QtGui.QColor(constructor)
3✔
405

406
        elif isinstance(constructor, (tuple, list)):
3✔
407
            return QtGui.QColor(*constructor)
3✔
408

409
        return None
×
410

411
    def get_format(self):
3✔
412
        """ Returns a QTextCharFormat that encodes the current style attributes.
413
        """
414
        format = QtGui.QTextCharFormat()
3✔
415

416
        # Set foreground color
417
        qcolor = self.get_color(self.foreground_color, self.intensity)
3✔
418
        if qcolor is not None:
3✔
419
            format.setForeground(qcolor)
3✔
420

421
        # Set background color
422
        qcolor = self.get_color(self.background_color, self.intensity)
3✔
423
        if qcolor is not None:
3✔
424
            format.setBackground(qcolor)
3✔
425

426
        # Set font weight/style options
427
        if self.bold:
3✔
428
            format.setFontWeight(QtGui.QFont.Bold)
3✔
429
        else:
430
            format.setFontWeight(QtGui.QFont.Normal)
3✔
431
        format.setFontItalic(self.italic)
3✔
432
        format.setFontUnderline(self.underline)
3✔
433

434
        return format
3✔
435

436
    def set_background_color(self, style):
3✔
437
        """
438
        Given a syntax style, attempt to set a color map that will be
439
        aesthetically pleasing.
440
        """
441
        # Set a new default color map.
442
        self.default_color_map = self.darkbg_color_map.copy()
3✔
443

444
        if not dark_style(style):
3✔
445
            # Colors appropriate for a terminal with a light background. For
446
            # now, only use non-bright colors...
447
            for i in range(8):
3✔
448
                self.default_color_map[i + 8] = self.default_color_map[i]
3✔
449

450
            # ...and replace white with black.
451
            self.default_color_map[7] = self.default_color_map[15] = 'black'
3✔
452

453
        # Update the current color map with the new defaults.
454
        self.color_map.update(self.default_color_map)
3✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc