1 # ANSI codes for 4 bit and xterm-256color
3 # Copyright (C) Andrew Bartlett 2018
5 # Originally written by Douglas Bagnall
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 # The 4 bit colours are available as global variables with names like
21 # RED, DARK_RED, REV_RED (for red background), and REV_DARK_RED. If
22 # switch_colour_off() is called, these names will all point to the
23 # empty string. switch_colour_on() restores the default values.
25 # The 256-colour codes are obtained using xterm_256_color(n), where n
26 # is the number of the desired colour.
29 def _gen_ansi_colours():
31 for i
, name
in enumerate(('BLACK', 'RED', 'GREEN', 'YELLOW', 'BLUE',
32 'MAGENTA', 'CYAN', 'WHITE')):
33 g
[name
] = "\033[1;3%dm" % i
34 g
['DARK_' + name
] = "\033[3%dm" % i
35 g
['REV_' + name
] = "\033[1;4%dm" % i
36 g
['REV_DARK_' + name
] = "\033[4%dm" % i
38 # kcc.debug uses these aliases (which make visual sense)
39 g
['PURPLE'] = DARK_MAGENTA
40 g
['GREY'] = DARK_WHITE
42 # C_NORMAL resets to normal, whatever that is
43 g
['C_NORMAL'] = "\033[0m"
45 # Non-colour ANSI codes.
46 g
['UNDERLINE'] = "\033[4m"
51 # Generate functions that colour a string. The functions look like
54 # c_BLUE("hello") # "\033[1;34mhello\033[0m" -> blue text
55 # c_DARK_RED(3) # 3 will be stringified and coloured
57 # but if colour is switched off, no colour codes are added.
59 # c_BLUE("hello") # "hello"
61 # The definition of the functions looks a little odd, because we want
62 # to bake in the name of the colour but not its actual value.
64 for _k
in list(globals().keys()):
67 return "%s%s%s" % (globals()[name
], s
, C_NORMAL
)
68 globals()['c_%s' % _k
] = _f
73 def switch_colour_off():
74 """Convert all the ANSI colour codes into empty strings."""
76 for k
, v
in list(g
.items()):
77 if k
.isupper() and isinstance(v
, str) and v
.startswith('\033'):
81 def switch_colour_on():
82 """Regenerate all the ANSI colour codes."""
86 def xterm_256_colour(n
, bg
=False, bold
=False):
87 weight
= '01;' if bold
else ''
88 target
= '48' if bg
else '38'
90 return "\033[%s%s;5;%dm" % (weight
, target
, int(n
))
93 def is_colour_wanted(*streams
, hint
='auto'):
94 """The hint is presumably a --color argument.
96 The streams to be considered can be file objects or file names,
97 with '-' being a special filename indicating stdout.
99 We follow the behaviour of GNU `ls` in what we accept.
100 * `git` is stricter, accepting only {always,never,auto}.
101 * `grep` is looser, accepting mixed case variants.
102 * historically we have used {yes,no,auto}.
103 * {always,never,auto} appears the commonest convention.
104 * if the caller tries to opt out of choosing and sets hint to None
105 or '', we assume 'auto'.
107 if hint
in ('no', 'never', 'none'):
110 if hint
in ('yes', 'always', 'force'):
113 if hint
not in ('auto', 'tty', 'if-tty', None, ''):
114 raise ValueError(f
"unexpected colour hint: {hint}; "
115 "try always|never|auto")
117 from os
import environ
118 if environ
.get('NO_COLOR'):
119 # Note: per spec, we treat the empty string as if unset.
122 for stream
in streams
:
123 if isinstance(stream
, str):
124 # This function can be passed filenames instead of file
125 # objects, in which case we treat '-' as stdout, and test
126 # that. Any other string is not regarded as a tty.
132 if not stream
.isatty():
137 def colour_if_wanted(*streams
, hint
='auto'):
138 wanted
= is_colour_wanted(*streams
, hint
=hint
)
146 def colourdiff(a
, b
):
147 """Generate a string comparing two strings or byte sequences, with
148 differences coloured to indicate what changed.
150 Byte sequences are printed as hex pairs separated by colons.
152 from difflib
import SequenceMatcher
154 if isinstance(a
, bytes
):
156 if isinstance(b
, bytes
):
158 a
= a
.replace(' ', '␠')
159 b
= b
.replace(' ', '␠')
161 s
= SequenceMatcher(None, a
, b
)
162 for op
, al
, ar
, bl
, br
in s
.get_opcodes():
164 out
.append(a
[al
: ar
])
166 out
.append(c_RED(a
[al
: ar
]))
168 out
.append(c_GREEN(b
[bl
: br
]))
169 elif op
== 'replace':
170 out
.append(c_RED(a
[al
: ar
]))
171 out
.append(c_GREEN(b
[bl
: br
]))
173 out
.append(f
' --unknown diff op {op}!-- ')