1# utility for color output for Mercurial commands
2#
3# Copyright (C) 2007 Kevin Christen <kevin.christen@gmail.com> and other
4#
5# This software may be used and distributed according to the terms of the
6# GNU General Public License version 2 or any later version.
7
8from __future__ import absolute_import
9
10import re
11
12from .i18n import _
13from .pycompat import getattr
14
15from . import (
16    encoding,
17    pycompat,
18)
19
20from .utils import stringutil
21
22try:
23    import curses
24
25    # Mapping from effect name to terminfo attribute name (or raw code) or
26    # color number.  This will also force-load the curses module.
27    _baseterminfoparams = {
28        b'none': (True, b'sgr0', b''),
29        b'standout': (True, b'smso', b''),
30        b'underline': (True, b'smul', b''),
31        b'reverse': (True, b'rev', b''),
32        b'inverse': (True, b'rev', b''),
33        b'blink': (True, b'blink', b''),
34        b'dim': (True, b'dim', b''),
35        b'bold': (True, b'bold', b''),
36        b'invisible': (True, b'invis', b''),
37        b'italic': (True, b'sitm', b''),
38        b'black': (False, curses.COLOR_BLACK, b''),
39        b'red': (False, curses.COLOR_RED, b''),
40        b'green': (False, curses.COLOR_GREEN, b''),
41        b'yellow': (False, curses.COLOR_YELLOW, b''),
42        b'blue': (False, curses.COLOR_BLUE, b''),
43        b'magenta': (False, curses.COLOR_MAGENTA, b''),
44        b'cyan': (False, curses.COLOR_CYAN, b''),
45        b'white': (False, curses.COLOR_WHITE, b''),
46    }
47except (ImportError, AttributeError):
48    curses = None
49    _baseterminfoparams = {}
50
51# start and stop parameters for effects
52_effects = {
53    b'none': 0,
54    b'black': 30,
55    b'red': 31,
56    b'green': 32,
57    b'yellow': 33,
58    b'blue': 34,
59    b'magenta': 35,
60    b'cyan': 36,
61    b'white': 37,
62    b'bold': 1,
63    b'italic': 3,
64    b'underline': 4,
65    b'inverse': 7,
66    b'dim': 2,
67    b'black_background': 40,
68    b'red_background': 41,
69    b'green_background': 42,
70    b'yellow_background': 43,
71    b'blue_background': 44,
72    b'purple_background': 45,
73    b'cyan_background': 46,
74    b'white_background': 47,
75}
76
77_defaultstyles = {
78    b'grep.match': b'red bold',
79    b'grep.linenumber': b'green',
80    b'grep.rev': b'blue',
81    b'grep.sep': b'cyan',
82    b'grep.filename': b'magenta',
83    b'grep.user': b'magenta',
84    b'grep.date': b'magenta',
85    b'grep.inserted': b'green bold',
86    b'grep.deleted': b'red bold',
87    b'bookmarks.active': b'green',
88    b'branches.active': b'none',
89    b'branches.closed': b'black bold',
90    b'branches.current': b'green',
91    b'branches.inactive': b'none',
92    b'diff.changed': b'white',
93    b'diff.deleted': b'red',
94    b'diff.deleted.changed': b'red bold underline',
95    b'diff.deleted.unchanged': b'red',
96    b'diff.diffline': b'bold',
97    b'diff.extended': b'cyan bold',
98    b'diff.file_a': b'red bold',
99    b'diff.file_b': b'green bold',
100    b'diff.hunk': b'magenta',
101    b'diff.inserted': b'green',
102    b'diff.inserted.changed': b'green bold underline',
103    b'diff.inserted.unchanged': b'green',
104    b'diff.tab': b'',
105    b'diff.trailingwhitespace': b'bold red_background',
106    b'changeset.public': b'',
107    b'changeset.draft': b'',
108    b'changeset.secret': b'',
109    b'diffstat.deleted': b'red',
110    b'diffstat.inserted': b'green',
111    b'formatvariant.name.mismatchconfig': b'red',
112    b'formatvariant.name.mismatchdefault': b'yellow',
113    b'formatvariant.name.uptodate': b'green',
114    b'formatvariant.repo.mismatchconfig': b'red',
115    b'formatvariant.repo.mismatchdefault': b'yellow',
116    b'formatvariant.repo.uptodate': b'green',
117    b'formatvariant.config.special': b'yellow',
118    b'formatvariant.config.default': b'green',
119    b'formatvariant.default': b'',
120    b'histedit.remaining': b'red bold',
121    b'ui.addremove.added': b'green',
122    b'ui.addremove.removed': b'red',
123    b'ui.error': b'red',
124    b'ui.prompt': b'yellow',
125    b'log.changeset': b'yellow',
126    b'patchbomb.finalsummary': b'',
127    b'patchbomb.from': b'magenta',
128    b'patchbomb.to': b'cyan',
129    b'patchbomb.subject': b'green',
130    b'patchbomb.diffstats': b'',
131    b'rebase.rebased': b'blue',
132    b'rebase.remaining': b'red bold',
133    b'resolve.resolved': b'green bold',
134    b'resolve.unresolved': b'red bold',
135    b'shelve.age': b'cyan',
136    b'shelve.newest': b'green bold',
137    b'shelve.name': b'blue bold',
138    b'status.added': b'green bold',
139    b'status.clean': b'none',
140    b'status.copied': b'none',
141    b'status.deleted': b'cyan bold underline',
142    b'status.ignored': b'black bold',
143    b'status.modified': b'blue bold',
144    b'status.removed': b'red bold',
145    b'status.unknown': b'magenta bold underline',
146    b'tags.normal': b'green',
147    b'tags.local': b'black bold',
148    b'upgrade-repo.requirement.preserved': b'cyan',
149    b'upgrade-repo.requirement.added': b'green',
150    b'upgrade-repo.requirement.removed': b'red',
151}
152
153
154def loadcolortable(ui, extname, colortable):
155    _defaultstyles.update(colortable)
156
157
158def _terminfosetup(ui, mode, formatted):
159    '''Initialize terminfo data and the terminal if we're in terminfo mode.'''
160
161    # If we failed to load curses, we go ahead and return.
162    if curses is None:
163        return
164    # Otherwise, see what the config file says.
165    if mode not in (b'auto', b'terminfo'):
166        return
167    ui._terminfoparams.update(_baseterminfoparams)
168
169    for key, val in ui.configitems(b'color'):
170        if key.startswith(b'color.'):
171            newval = (False, int(val), b'')
172            ui._terminfoparams[key[6:]] = newval
173        elif key.startswith(b'terminfo.'):
174            newval = (True, b'', val.replace(b'\\E', b'\x1b'))
175            ui._terminfoparams[key[9:]] = newval
176    try:
177        curses.setupterm()
178    except curses.error:
179        ui._terminfoparams.clear()
180        return
181
182    for key, (b, e, c) in ui._terminfoparams.copy().items():
183        if not b:
184            continue
185        if not c and not curses.tigetstr(pycompat.sysstr(e)):
186            # Most terminals don't support dim, invis, etc, so don't be
187            # noisy and use ui.debug().
188            ui.debug(b"no terminfo entry for %s\n" % e)
189            del ui._terminfoparams[key]
190    if not curses.tigetstr('setaf') or not curses.tigetstr('setab'):
191        # Only warn about missing terminfo entries if we explicitly asked for
192        # terminfo mode and we're in a formatted terminal.
193        if mode == b"terminfo" and formatted:
194            ui.warn(
195                _(
196                    b"no terminfo entry for setab/setaf: reverting to "
197                    b"ECMA-48 color\n"
198                )
199            )
200        ui._terminfoparams.clear()
201
202
203def setup(ui):
204    """configure color on a ui
205
206    That function both set the colormode for the ui object and read
207    the configuration looking for custom colors and effect definitions."""
208    mode = _modesetup(ui)
209    ui._colormode = mode
210    if mode and mode != b'debug':
211        configstyles(ui)
212
213
214def _modesetup(ui):
215    if ui.plain(b'color'):
216        return None
217    config = ui.config(b'ui', b'color')
218    if config == b'debug':
219        return b'debug'
220
221    auto = config == b'auto'
222    always = False
223    if not auto and stringutil.parsebool(config):
224        # We want the config to behave like a boolean, "on" is actually auto,
225        # but "always" value is treated as a special case to reduce confusion.
226        if (
227            ui.configsource(b'ui', b'color') == b'--color'
228            or config == b'always'
229        ):
230            always = True
231        else:
232            auto = True
233
234    if not always and not auto:
235        return None
236
237    formatted = always or (
238        encoding.environ.get(b'TERM') != b'dumb' and ui.formatted()
239    )
240
241    mode = ui.config(b'color', b'mode')
242
243    # If pager is active, color.pagermode overrides color.mode.
244    if getattr(ui, 'pageractive', False):
245        mode = ui.config(b'color', b'pagermode', mode)
246
247    realmode = mode
248    if pycompat.iswindows:
249        from . import win32
250
251        term = encoding.environ.get(b'TERM')
252        # TERM won't be defined in a vanilla cmd.exe environment.
253
254        # UNIX-like environments on Windows such as Cygwin and MSYS will
255        # set TERM. They appear to make a best effort attempt at setting it
256        # to something appropriate. However, not all environments with TERM
257        # defined support ANSI.
258        ansienviron = term and b'xterm' in term
259
260        if mode == b'auto':
261            # Since "ansi" could result in terminal gibberish, we error on the
262            # side of selecting "win32". However, if w32effects is not defined,
263            # we almost certainly don't support "win32", so don't even try.
264            # w32effects is not populated when stdout is redirected, so checking
265            # it first avoids win32 calls in a state known to error out.
266            if ansienviron or not w32effects or win32.enablevtmode():
267                realmode = b'ansi'
268            else:
269                realmode = b'win32'
270        # An empty w32effects is a clue that stdout is redirected, and thus
271        # cannot enable VT mode.
272        elif mode == b'ansi' and w32effects and not ansienviron:
273            win32.enablevtmode()
274    elif mode == b'auto':
275        realmode = b'ansi'
276
277    def modewarn():
278        # only warn if color.mode was explicitly set and we're in
279        # a formatted terminal
280        if mode == realmode and formatted:
281            ui.warn(_(b'warning: failed to set color mode to %s\n') % mode)
282
283    if realmode == b'win32':
284        ui._terminfoparams.clear()
285        if not w32effects:
286            modewarn()
287            return None
288    elif realmode == b'ansi':
289        ui._terminfoparams.clear()
290    elif realmode == b'terminfo':
291        _terminfosetup(ui, mode, formatted)
292        if not ui._terminfoparams:
293            ## FIXME Shouldn't we return None in this case too?
294            modewarn()
295            realmode = b'ansi'
296    else:
297        return None
298
299    if always or (auto and formatted):
300        return realmode
301    return None
302
303
304def configstyles(ui):
305    ui._styles.update(_defaultstyles)
306    for status, cfgeffects in ui.configitems(b'color'):
307        if b'.' not in status or status.startswith((b'color.', b'terminfo.')):
308            continue
309        cfgeffects = ui.configlist(b'color', status)
310        if cfgeffects:
311            good = []
312            for e in cfgeffects:
313                if valideffect(ui, e):
314                    good.append(e)
315                else:
316                    ui.warn(
317                        _(
318                            b"ignoring unknown color/effect %s "
319                            b"(configured in color.%s)\n"
320                        )
321                        % (stringutil.pprint(e), status)
322                    )
323            ui._styles[status] = b' '.join(good)
324
325
326def _activeeffects(ui):
327    '''Return the effects map for the color mode set on the ui.'''
328    if ui._colormode == b'win32':
329        return w32effects
330    elif ui._colormode is not None:
331        return _effects
332    return {}
333
334
335def valideffect(ui, effect):
336    """Determine if the effect is valid or not."""
337    return (not ui._terminfoparams and effect in _activeeffects(ui)) or (
338        effect in ui._terminfoparams or effect[:-11] in ui._terminfoparams
339    )
340
341
342def _effect_str(ui, effect):
343    '''Helper function for render_effects().'''
344
345    bg = False
346    if effect.endswith(b'_background'):
347        bg = True
348        effect = effect[:-11]
349    try:
350        attr, val, termcode = ui._terminfoparams[effect]
351    except KeyError:
352        return b''
353    if attr:
354        if termcode:
355            return termcode
356        else:
357            return curses.tigetstr(pycompat.sysstr(val))
358    elif bg:
359        return curses.tparm(curses.tigetstr('setab'), val)
360    else:
361        return curses.tparm(curses.tigetstr('setaf'), val)
362
363
364def _mergeeffects(text, start, stop):
365    """Insert start sequence at every occurrence of stop sequence
366
367    >>> s = _mergeeffects(b'cyan', b'[C]', b'|')
368    >>> s = _mergeeffects(s + b'yellow', b'[Y]', b'|')
369    >>> s = _mergeeffects(b'ma' + s + b'genta', b'[M]', b'|')
370    >>> s = _mergeeffects(b'red' + s, b'[R]', b'|')
371    >>> s
372    '[R]red[M]ma[Y][C]cyan|[R][M][Y]yellow|[R][M]genta|'
373    """
374    parts = []
375    for t in text.split(stop):
376        if not t:
377            continue
378        parts.extend([start, t, stop])
379    return b''.join(parts)
380
381
382def _render_effects(ui, text, effects):
383    """Wrap text in commands to turn on each effect."""
384    if not text:
385        return text
386    if ui._terminfoparams:
387        start = b''.join(
388            _effect_str(ui, effect) for effect in [b'none'] + effects.split()
389        )
390        stop = _effect_str(ui, b'none')
391    else:
392        activeeffects = _activeeffects(ui)
393        start = [
394            pycompat.bytestr(activeeffects[e])
395            for e in [b'none'] + effects.split()
396        ]
397        start = b'\033[' + b';'.join(start) + b'm'
398        stop = b'\033[' + pycompat.bytestr(activeeffects[b'none']) + b'm'
399    return _mergeeffects(text, start, stop)
400
401
402_ansieffectre = re.compile(br'\x1b\[[0-9;]*m')
403
404
405def stripeffects(text):
406    """Strip ANSI control codes which could be inserted by colorlabel()"""
407    return _ansieffectre.sub(b'', text)
408
409
410def colorlabel(ui, msg, label):
411    """add color control code according to the mode"""
412    if ui._colormode == b'debug':
413        if label and msg:
414            if msg.endswith(b'\n'):
415                msg = b"[%s|%s]\n" % (label, msg[:-1])
416            else:
417                msg = b"[%s|%s]" % (label, msg)
418    elif ui._colormode is not None:
419        effects = []
420        for l in label.split():
421            s = ui._styles.get(l, b'')
422            if s:
423                effects.append(s)
424            elif valideffect(ui, l):
425                effects.append(l)
426        effects = b' '.join(effects)
427        if effects:
428            msg = b'\n'.join(
429                [
430                    _render_effects(ui, line, effects)
431                    for line in msg.split(b'\n')
432                ]
433            )
434    return msg
435
436
437w32effects = None
438if pycompat.iswindows:
439    import ctypes
440
441    _kernel32 = ctypes.windll.kernel32  # pytype: disable=module-attr
442
443    _WORD = ctypes.c_ushort
444
445    _INVALID_HANDLE_VALUE = -1
446
447    class _COORD(ctypes.Structure):
448        _fields_ = [('X', ctypes.c_short), ('Y', ctypes.c_short)]
449
450    class _SMALL_RECT(ctypes.Structure):
451        _fields_ = [
452            ('Left', ctypes.c_short),
453            ('Top', ctypes.c_short),
454            ('Right', ctypes.c_short),
455            ('Bottom', ctypes.c_short),
456        ]
457
458    class _CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure):
459        _fields_ = [
460            ('dwSize', _COORD),
461            ('dwCursorPosition', _COORD),
462            ('wAttributes', _WORD),
463            ('srWindow', _SMALL_RECT),
464            ('dwMaximumWindowSize', _COORD),
465        ]
466
467    _STD_OUTPUT_HANDLE = 0xFFFFFFF5  # (DWORD)-11
468    _STD_ERROR_HANDLE = 0xFFFFFFF4  # (DWORD)-12
469
470    _FOREGROUND_BLUE = 0x0001
471    _FOREGROUND_GREEN = 0x0002
472    _FOREGROUND_RED = 0x0004
473    _FOREGROUND_INTENSITY = 0x0008
474
475    _BACKGROUND_BLUE = 0x0010
476    _BACKGROUND_GREEN = 0x0020
477    _BACKGROUND_RED = 0x0040
478    _BACKGROUND_INTENSITY = 0x0080
479
480    _COMMON_LVB_REVERSE_VIDEO = 0x4000
481    _COMMON_LVB_UNDERSCORE = 0x8000
482
483    # http://msdn.microsoft.com/en-us/library/ms682088%28VS.85%29.aspx
484    w32effects = {
485        b'none': -1,
486        b'black': 0,
487        b'red': _FOREGROUND_RED,
488        b'green': _FOREGROUND_GREEN,
489        b'yellow': _FOREGROUND_RED | _FOREGROUND_GREEN,
490        b'blue': _FOREGROUND_BLUE,
491        b'magenta': _FOREGROUND_BLUE | _FOREGROUND_RED,
492        b'cyan': _FOREGROUND_BLUE | _FOREGROUND_GREEN,
493        b'white': _FOREGROUND_RED | _FOREGROUND_GREEN | _FOREGROUND_BLUE,
494        b'bold': _FOREGROUND_INTENSITY,
495        b'black_background': 0x100,  # unused value > 0x0f
496        b'red_background': _BACKGROUND_RED,
497        b'green_background': _BACKGROUND_GREEN,
498        b'yellow_background': _BACKGROUND_RED | _BACKGROUND_GREEN,
499        b'blue_background': _BACKGROUND_BLUE,
500        b'purple_background': _BACKGROUND_BLUE | _BACKGROUND_RED,
501        b'cyan_background': _BACKGROUND_BLUE | _BACKGROUND_GREEN,
502        b'white_background': (
503            _BACKGROUND_RED | _BACKGROUND_GREEN | _BACKGROUND_BLUE
504        ),
505        b'bold_background': _BACKGROUND_INTENSITY,
506        b'underline': _COMMON_LVB_UNDERSCORE,  # double-byte charsets only
507        b'inverse': _COMMON_LVB_REVERSE_VIDEO,  # double-byte charsets only
508    }
509
510    passthrough = {
511        _FOREGROUND_INTENSITY,
512        _BACKGROUND_INTENSITY,
513        _COMMON_LVB_UNDERSCORE,
514        _COMMON_LVB_REVERSE_VIDEO,
515    }
516
517    stdout = _kernel32.GetStdHandle(
518        _STD_OUTPUT_HANDLE
519    )  # don't close the handle returned
520    if stdout is None or stdout == _INVALID_HANDLE_VALUE:
521        w32effects = None
522    else:
523        csbi = _CONSOLE_SCREEN_BUFFER_INFO()
524        if not _kernel32.GetConsoleScreenBufferInfo(stdout, ctypes.byref(csbi)):
525            # stdout may not support GetConsoleScreenBufferInfo()
526            # when called from subprocess or redirected
527            w32effects = None
528        else:
529            origattr = csbi.wAttributes
530            ansire = re.compile(
531                br'\033\[([^m]*)m([^\033]*)(.*)', re.MULTILINE | re.DOTALL
532            )
533
534    def win32print(ui, writefunc, text, **opts):
535        label = opts.get('label', b'')
536        attr = origattr
537
538        def mapcolor(val, attr):
539            if val == -1:
540                return origattr
541            elif val in passthrough:
542                return attr | val
543            elif val > 0x0F:
544                return (val & 0x70) | (attr & 0x8F)
545            else:
546                return (val & 0x07) | (attr & 0xF8)
547
548        # determine console attributes based on labels
549        for l in label.split():
550            style = ui._styles.get(l, b'')
551            for effect in style.split():
552                try:
553                    attr = mapcolor(w32effects[effect], attr)
554                except KeyError:
555                    # w32effects could not have certain attributes so we skip
556                    # them if not found
557                    pass
558        # hack to ensure regexp finds data
559        if not text.startswith(b'\033['):
560            text = b'\033[m' + text
561
562        # Look for ANSI-like codes embedded in text
563        m = re.match(ansire, text)
564
565        try:
566            while m:
567                for sattr in m.group(1).split(b';'):
568                    if sattr:
569                        attr = mapcolor(int(sattr), attr)
570                ui.flush()
571                _kernel32.SetConsoleTextAttribute(stdout, attr)
572                writefunc(m.group(2))
573                m = re.match(ansire, m.group(3))
574        finally:
575            # Explicitly reset original attributes
576            ui.flush()
577            _kernel32.SetConsoleTextAttribute(stdout, origattr)
578