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

jupyter / qtconsole / 9951678590

16 Jul 2024 06:11AM UTC coverage: 61.817% (-0.009%) from 61.826%
9951678590

Pull #611

github

web-flow
Merge 5a4874791 into 56e5a5ec5
Pull Request #611: [WIP] PR: Change ANSI color code 3 mapping from `brown` to `gold` and set bold format processing enabled by default

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

2 existing lines in 1 file now uncovered.

2898 of 4688 relevant lines covered (61.82%)

1.85 hits per line

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

90.24
/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 erase requests (ED and EL commands).
22
EraseAction = namedtuple('EraseAction', ['action', 'area', 'erase_to'])
3✔
23

24
# An action for cursor move requests (CUU, CUD, CUF, CUB, CNL, CPL, CHA, CUP,
25
# and HVP commands).
26
# FIXME: Not implemented in AnsiCodeProcessor.
27
MoveAction = namedtuple('MoveAction', ['action', 'dir', 'unit', 'count'])
3✔
28

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

32
# An action for the carriage return character
33
CarriageReturnAction = namedtuple('CarriageReturnAction', ['action'])
3✔
34

35
# An action for the \n character
36
NewLineAction = namedtuple('NewLineAction', ['action'])
3✔
37

38
# An action for the beep character
39
BeepAction = namedtuple('BeepAction', ['action'])
3✔
40

41
# An action for backspace
42
BackSpaceAction = namedtuple('BackSpaceAction', ['action'])
3✔
43

44
# Regular expressions.
45
CSI_COMMANDS = 'ABCDEFGHJKSTfmnsu'
3✔
46
CSI_SUBPATTERN = '\\[(.*?)([%s])' % CSI_COMMANDS
3✔
47
OSC_SUBPATTERN = '\\](.*?)[\x07\x1b]'
3✔
48
ANSI_PATTERN = ('\x01?\x1b(%s|%s)\x02?' % \
3✔
49
                (CSI_SUBPATTERN, OSC_SUBPATTERN))
50
ANSI_OR_SPECIAL_PATTERN = re.compile('(\a|\b|\r(?!\n)|\r?\n)|(?:%s)' % ANSI_PATTERN)
3✔
51
SPECIAL_PATTERN = re.compile('([\f])')
3✔
52

53
#-----------------------------------------------------------------------------
54
# Classes
55
#-----------------------------------------------------------------------------
56

57
class AnsiCodeProcessor(object):
3✔
58
    """ Translates special ASCII characters and ANSI escape codes into readable
59
        attributes. It also supports a few non-standard, xterm-specific codes.
60
    """
61

62
    # Whether to increase intensity or set boldness for SGR code 1.
63
    # (Different terminals handle this in different ways.)
64
    bold_text_enabled = True
3✔
65

66
    # We provide an empty default color map because subclasses will likely want
67
    # to use a custom color format.
68
    default_color_map = {}
3✔
69

70
    #---------------------------------------------------------------------------
71
    # AnsiCodeProcessor interface
72
    #---------------------------------------------------------------------------
73

74
    def __init__(self):
3✔
75
        self.actions = []
3✔
76
        self.color_map = self.default_color_map.copy()
3✔
77
        self.reset_sgr()
3✔
78

79
    def reset_sgr(self):
3✔
80
        """ Reset graphics attributs to their default values.
81
        """
82
        self.intensity = 0
3✔
83
        self.italic = False
3✔
84
        self.bold = False
3✔
85
        self.underline = False
3✔
86
        self.foreground_color = None
3✔
87
        self.background_color = None
3✔
88

89
    def split_string(self, string):
3✔
90
        """ Yields substrings for which the same escape code applies.
91
        """
92
        self.actions = []
3✔
93
        start = 0
3✔
94

95
        # strings ending with \r are assumed to be ending in \r\n since
96
        # \n is appended to output strings automatically.  Accounting
97
        # for that, here.
98
        last_char = '\n' if len(string) > 0 and string[-1] == '\n' else None
3✔
99
        string = string[:-1] if last_char is not None else string
3✔
100

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

109
            groups = [g for g in match.groups() if (g is not None)]
3✔
110
            g0 = groups[0]
3✔
111
            if g0 == '\a':
3✔
112
                self.actions.append(BeepAction('beep'))
3✔
113
                yield None
3✔
114
                self.actions = []
3✔
115
            elif g0 == '\r':
3✔
116
                self.actions.append(CarriageReturnAction('carriage-return'))
3✔
117
                yield None
3✔
118
                self.actions = []
3✔
119
            elif g0 == '\b':
3✔
120
                self.actions.append(BackSpaceAction('backspace'))
3✔
121
                yield None
3✔
122
                self.actions = []
3✔
123
            elif g0 == '\n' or g0 == '\r\n':
3✔
124
                self.actions.append(NewLineAction('newline'))
3✔
125
                yield g0
3✔
126
                self.actions = []
3✔
127
            else:
128
                params = [ param for param in groups[1].split(';') if param ]
3✔
129
                if g0.startswith('['):
3✔
130
                    # Case 1: CSI code.
131
                    try:
3✔
132
                        params = list(map(int, params))
3✔
133
                    except ValueError:
×
134
                        # Silently discard badly formed codes.
135
                        pass
×
136
                    else:
137
                        self.set_csi_code(groups[2], params)
3✔
138

139
                elif g0.startswith(']'):
3✔
140
                    # Case 2: OSC code.
141
                    self.set_osc_code(params)
3✔
142

143
        raw = string[start:]
3✔
144
        substring = SPECIAL_PATTERN.sub(self._replace_special, raw)
3✔
145
        if substring or self.actions:
3✔
146
            yield substring
3✔
147

148
        if last_char is not None:
3✔
149
            self.actions.append(NewLineAction('newline'))
3✔
150
            yield last_char
3✔
151

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

155
        Parameters
156
        ----------
157
        command : str
158
            The code identifier, i.e. the final character in the sequence.
159

160
        params : sequence of integers, optional
161
            The parameter codes for the command.
162
        """
163
        if command == 'm':   # SGR - Select Graphic Rendition
3✔
164
            if params:
3✔
165
                self.set_sgr_code(params)
3✔
166
            else:
167
                self.set_sgr_code([0])
×
168

169
        elif (command == 'J' or # ED - Erase Data
3✔
170
              command == 'K'):  # EL - Erase in Line
171
            code = params[0] if params else 0
3✔
172
            if 0 <= code <= 2:
3✔
173
                area = 'screen' if command == 'J' else 'line'
3✔
174
                if code == 0:
3✔
175
                    erase_to = 'end'
3✔
176
                elif code == 1:
3✔
177
                    erase_to = 'start'
3✔
178
                elif code == 2:
3✔
179
                    erase_to = 'all'
3✔
180
                self.actions.append(EraseAction('erase', area, erase_to))
3✔
181

182
        elif (command == 'S' or # SU - Scroll Up
3✔
183
              command == 'T'):  # SD - Scroll Down
184
            dir = 'up' if command == 'S' else 'down'
3✔
185
            count = params[0] if params else 1
3✔
186
            self.actions.append(ScrollAction('scroll', dir, 'line', count))
3✔
187

188
    def set_osc_code(self, params):
3✔
189
        """ Set attributes based on OSC (Operating System Command) parameters.
190

191
        Parameters
192
        ----------
193
        params : sequence of str
194
            The parameters for the command.
195
        """
196
        try:
3✔
197
            command = int(params.pop(0))
3✔
198
        except (IndexError, ValueError):
×
199
            return
×
200

201
        if command == 4:
3✔
202
            # xterm-specific: set color number to color spec.
203
            try:
3✔
204
                color = int(params.pop(0))
3✔
205
                spec = params.pop(0)
3✔
206
                self.color_map[color] = self._parse_xterm_color_spec(spec)
3✔
207
            except (IndexError, ValueError):
×
208
                pass
×
209

210
    def set_sgr_code(self, params):
3✔
211
        """ Set attributes based on SGR (Select Graphic Rendition) codes.
212

213
        Parameters
214
        ----------
215
        params : sequence of ints
216
            A list of SGR codes for one or more SGR commands. Usually this
217
            sequence will have one element per command, although certain
218
            xterm-specific commands requires multiple elements.
219
        """
220
        # Always consume the first parameter.
221
        if not params:
3✔
222
            return
3✔
223
        code = params.pop(0)
3✔
224

225
        if code == 0:
3✔
226
            self.reset_sgr()
3✔
227
        elif code == 1:
3✔
228
            if self.bold_text_enabled:
3✔
229
                self.bold = True
3✔
230
            else:
UNCOV
231
                self.intensity = 1
×
232
        elif code == 2:
3✔
233
            self.intensity = 0
×
234
        elif code == 3:
3✔
235
            self.italic = True
1✔
236
        elif code == 4:
3✔
237
            self.underline = True
×
238
        elif code == 22:
3✔
239
            self.intensity = 0
×
240
            self.bold = False
×
241
        elif code == 23:
3✔
242
            self.italic = False
×
243
        elif code == 24:
3✔
244
            self.underline = False
×
245
        elif code >= 30 and code <= 37:
3✔
246
            self.foreground_color = code - 30
3✔
247
        elif code == 38 and params:
3✔
248
            _color_type = params.pop(0)
3✔
249
            if _color_type == 5 and params:
3✔
250
                # xterm-specific: 256 color support.
251
                self.foreground_color = params.pop(0)
3✔
252
            elif _color_type == 2:
3✔
253
                # 24bit true colour support.
254
                self.foreground_color = params[:3]
3✔
255
                params[:3] = []
3✔
256
        elif code == 39:
3✔
257
            self.foreground_color = None
3✔
258
        elif code >= 40 and code <= 47:
3✔
259
            self.background_color = code - 40
3✔
260
        elif code == 48 and params:
3✔
261
            _color_type = params.pop(0)
3✔
262
            if _color_type == 5 and params:
3✔
263
                # xterm-specific: 256 color support.
264
                self.background_color = params.pop(0)
3✔
265
            elif _color_type == 2:
3✔
266
                # 24bit true colour support.
267
                self.background_color = params[:3]
3✔
268
                params[:3] = []
3✔
269
        elif code == 49:
3✔
270
            self.background_color = None
3✔
271

272
        # Recurse with unconsumed parameters.
273
        self.set_sgr_code(params)
3✔
274

275
    #---------------------------------------------------------------------------
276
    # Protected interface
277
    #---------------------------------------------------------------------------
278

279
    def _parse_xterm_color_spec(self, spec):
3✔
280
        if spec.startswith('rgb:'):
3✔
281
            return tuple(map(lambda x: int(x, 16), spec[4:].split('/')))
3✔
282
        elif spec.startswith('rgbi:'):
3✔
283
            return tuple(map(lambda x: int(float(x) * 255),
3✔
284
                             spec[5:].split('/')))
285
        elif spec == '?':
×
286
            raise ValueError('Unsupported xterm color spec')
×
287
        return spec
×
288

289
    def _replace_special(self, match):
3✔
290
        special = match.group(1)
3✔
291
        if special == '\f':
3✔
292
            self.actions.append(ScrollAction('scroll', 'down', 'page', 1))
3✔
293
        return ''
3✔
294

295

296
class QtAnsiCodeProcessor(AnsiCodeProcessor):
3✔
297
    """ Translates ANSI escape codes into QTextCharFormats.
298
    """
299

300
    # A map from ANSI color codes to SVG color names or RGB(A) tuples.
301
    darkbg_color_map = {
3✔
302
        0  : 'black',       # black
303
        1  : 'darkred',     # red
304
        2  : 'darkgreen',   # green
305
        3  : 'gold',       # yellow
306
        4  : 'darkblue',    # blue
307
        5  : 'darkviolet',  # magenta
308
        6  : 'steelblue',   # cyan
309
        7  : 'grey',        # white
310
        8  : 'grey',        # black (bright)
311
        9  : 'red',         # red (bright)
312
        10 : 'lime',        # green (bright)
313
        11 : 'yellow',      # yellow (bright)
314
        12 : 'deepskyblue', # blue (bright)
315
        13 : 'magenta',     # magenta (bright)
316
        14 : 'cyan',        # cyan (bright)
317
        15 : 'white' }      # white (bright)
318

319
    # Set the default color map for super class.
320
    default_color_map = darkbg_color_map.copy()
3✔
321

322
    def get_color(self, color, intensity=0):
3✔
323
        """ Returns a QColor for a given color code or rgb list, or None if one
324
            cannot be constructed.
325
        """
326

327
        if isinstance(color, int):
3✔
328
            # Adjust for intensity, if possible.
329
            if color < 8 and intensity > 0:
3✔
UNCOV
330
                color += 8
×
331
            constructor = self.color_map.get(color, None)
3✔
332
        elif isinstance(color, (tuple, list)):
3✔
333
            constructor = color
×
334
        else:
335
            return None
3✔
336

337
        if isinstance(constructor, str):
3✔
338
            # If this is an X11 color name, we just hope there is a close SVG
339
            # color name. We could use QColor's static method
340
            # 'setAllowX11ColorNames()', but this is global and only available
341
            # on X11. It seems cleaner to aim for uniformity of behavior.
342
            return QtGui.QColor(constructor)
3✔
343

344
        elif isinstance(constructor, (tuple, list)):
3✔
345
            return QtGui.QColor(*constructor)
×
346

347
        return None
3✔
348

349
    def get_format(self):
3✔
350
        """ Returns a QTextCharFormat that encodes the current style attributes.
351
        """
352
        format = QtGui.QTextCharFormat()
3✔
353

354
        # Set foreground color
355
        qcolor = self.get_color(self.foreground_color, self.intensity)
3✔
356
        if qcolor is not None:
3✔
357
            format.setForeground(qcolor)
3✔
358

359
        # Set background color
360
        qcolor = self.get_color(self.background_color, self.intensity)
3✔
361
        if qcolor is not None:
3✔
362
            format.setBackground(qcolor)
3✔
363

364
        # Set font weight/style options
365
        if self.bold:
3✔
366
            format.setFontWeight(QtGui.QFont.Bold)
3✔
367
        else:
368
            format.setFontWeight(QtGui.QFont.Normal)
3✔
369
        format.setFontItalic(self.italic)
3✔
370
        format.setFontUnderline(self.underline)
3✔
371

372
        return format
3✔
373

374
    def set_background_color(self, style):
3✔
375
        """
376
        Given a syntax style, attempt to set a color map that will be
377
        aesthetically pleasing.
378
        """
379
        # Set a new default color map.
380
        self.default_color_map = self.darkbg_color_map.copy()
3✔
381

382
        if not dark_style(style):
3✔
383
            # Colors appropriate for a terminal with a light background. For
384
            # now, only use non-bright colors...
385
            for i in range(8):
3✔
386
                self.default_color_map[i + 8] = self.default_color_map[i]
3✔
387

388
            # ...and replace white with black.
389
            self.default_color_map[7] = self.default_color_map[15] = 'black'
3✔
390

391
        # Update the current color map with the new defaults.
392
        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

© 2025 Coveralls, Inc