skilleter-thingy 0.0.40__py3-none-any.whl → 0.0.41__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of skilleter-thingy might be problematic. Click here for more details.
- skilleter_thingy/__init__.py +6 -0
- skilleter_thingy/addpath.py +107 -0
- skilleter_thingy/borger.py +269 -0
- skilleter_thingy/console_colours.py +63 -0
- skilleter_thingy/diskspacecheck.py +67 -0
- skilleter_thingy/docker_purge.py +113 -0
- skilleter_thingy/ffind.py +536 -0
- skilleter_thingy/ggit.py +90 -0
- skilleter_thingy/ggrep.py +154 -0
- skilleter_thingy/git_br.py +180 -0
- skilleter_thingy/git_ca.py +142 -0
- skilleter_thingy/git_cleanup.py +287 -0
- skilleter_thingy/git_co.py +220 -0
- skilleter_thingy/git_common.py +61 -0
- skilleter_thingy/git_hold.py +154 -0
- skilleter_thingy/git_mr.py +92 -0
- skilleter_thingy/git_parent.py +77 -0
- skilleter_thingy/git_review.py +1428 -0
- skilleter_thingy/git_update.py +385 -0
- skilleter_thingy/git_wt.py +96 -0
- skilleter_thingy/gitcmp_helper.py +322 -0
- skilleter_thingy/gitprompt.py +274 -0
- skilleter_thingy/gl.py +174 -0
- skilleter_thingy/gphotosync.py +610 -0
- skilleter_thingy/linecount.py +155 -0
- skilleter_thingy/moviemover.py +133 -0
- skilleter_thingy/photodupe.py +136 -0
- skilleter_thingy/phototidier.py +248 -0
- skilleter_thingy/py_audit.py +131 -0
- skilleter_thingy/readable.py +270 -0
- skilleter_thingy/remdir.py +126 -0
- skilleter_thingy/rmdupe.py +550 -0
- skilleter_thingy/rpylint.py +91 -0
- skilleter_thingy/splitpics.py +99 -0
- skilleter_thingy/strreplace.py +82 -0
- skilleter_thingy/sysmon.py +435 -0
- skilleter_thingy/tfm.py +920 -0
- skilleter_thingy/tfparse.py +101 -0
- skilleter_thingy/thingy/__init__.py +6 -0
- skilleter_thingy/thingy/colour.py +213 -0
- skilleter_thingy/thingy/dc_curses.py +278 -0
- skilleter_thingy/thingy/dc_defaults.py +221 -0
- skilleter_thingy/thingy/dc_util.py +50 -0
- skilleter_thingy/thingy/dircolors.py +308 -0
- skilleter_thingy/thingy/docker.py +95 -0
- skilleter_thingy/thingy/files.py +142 -0
- skilleter_thingy/thingy/git.py +1371 -0
- skilleter_thingy/thingy/git2.py +1307 -0
- skilleter_thingy/thingy/gitlab.py +193 -0
- skilleter_thingy/thingy/logger.py +112 -0
- skilleter_thingy/thingy/path.py +156 -0
- skilleter_thingy/thingy/popup.py +87 -0
- skilleter_thingy/thingy/process.py +112 -0
- skilleter_thingy/thingy/run.py +334 -0
- skilleter_thingy/thingy/tfm_pane.py +595 -0
- skilleter_thingy/thingy/tidy.py +160 -0
- skilleter_thingy/trimpath.py +84 -0
- skilleter_thingy/window_rename.py +92 -0
- skilleter_thingy/xchmod.py +125 -0
- skilleter_thingy/yamlcheck.py +89 -0
- {skilleter_thingy-0.0.40.dist-info → skilleter_thingy-0.0.41.dist-info}/METADATA +1 -1
- skilleter_thingy-0.0.41.dist-info/RECORD +66 -0
- skilleter_thingy-0.0.41.dist-info/top_level.txt +1 -0
- skilleter_thingy-0.0.40.dist-info/RECORD +0 -6
- skilleter_thingy-0.0.40.dist-info/top_level.txt +0 -1
- {skilleter_thingy-0.0.40.dist-info → skilleter_thingy-0.0.41.dist-info}/LICENSE +0 -0
- {skilleter_thingy-0.0.40.dist-info → skilleter_thingy-0.0.41.dist-info}/WHEEL +0 -0
- {skilleter_thingy-0.0.40.dist-info → skilleter_thingy-0.0.41.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#! /usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Read JSON Terraform output and convert back to human-readable text
|
|
5
|
+
This allows multiple errors and warnings to be reported as there's
|
|
6
|
+
no way of doing this directly from Terraform
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import json
|
|
12
|
+
import argparse
|
|
13
|
+
from collections import defaultdict
|
|
14
|
+
|
|
15
|
+
import thingy.colour as colour
|
|
16
|
+
|
|
17
|
+
################################################################################
|
|
18
|
+
|
|
19
|
+
def error(msg, status=1):
|
|
20
|
+
"""Report an error and quit"""
|
|
21
|
+
|
|
22
|
+
colour.write(f'[RED:ERROR]: {msg}')
|
|
23
|
+
sys.exit(status)
|
|
24
|
+
|
|
25
|
+
################################################################################
|
|
26
|
+
|
|
27
|
+
def main():
|
|
28
|
+
"""Everything"""
|
|
29
|
+
|
|
30
|
+
# Command line is either empty or contains the input file
|
|
31
|
+
|
|
32
|
+
parser = argparse.ArgumentParser(description='Convert Terraform JSON output back into human-readable text')
|
|
33
|
+
parser.add_argument('--abspath', '-a', action='store_true', help='Output absolute file paths')
|
|
34
|
+
parser.add_argument('infile', nargs='*', help='The error file (defaults to standard input if not specified)')
|
|
35
|
+
|
|
36
|
+
args = parser.parse_args()
|
|
37
|
+
|
|
38
|
+
# Open the input file or use stdin and read the JSON
|
|
39
|
+
|
|
40
|
+
jsonfile = open(sys.argv[1], 'rt') if args.infile else sys.stdin
|
|
41
|
+
|
|
42
|
+
terraform = json.loads(jsonfile.read())
|
|
43
|
+
|
|
44
|
+
# Collect each of the error/warnings
|
|
45
|
+
|
|
46
|
+
report = defaultdict(list)
|
|
47
|
+
|
|
48
|
+
if 'diagnostics' in terraform:
|
|
49
|
+
for diagnostics in terraform['diagnostics']:
|
|
50
|
+
severity = diagnostics['severity'].title()
|
|
51
|
+
|
|
52
|
+
if 'range' in diagnostics:
|
|
53
|
+
file_path = os.path.abspath(diagnostics['range']['filename']) if args.abspath else diagnostics['range']['filename']
|
|
54
|
+
|
|
55
|
+
category = f'{severity}: {diagnostics["summary"]} - {diagnostics["detail"]}'
|
|
56
|
+
|
|
57
|
+
message = ''
|
|
58
|
+
if 'range' in diagnostics:
|
|
59
|
+
message += f'In [BLUE:{file_path}:{diagnostics["range"]["start"]["line"]}]'
|
|
60
|
+
|
|
61
|
+
if 'address' in diagnostics:
|
|
62
|
+
message += f' in [BLUE:{diagnostics["address"]}]'
|
|
63
|
+
|
|
64
|
+
report[category].append(message)
|
|
65
|
+
|
|
66
|
+
for category in report:
|
|
67
|
+
colour.write()
|
|
68
|
+
|
|
69
|
+
# Fudge emboldening multi-line warnings
|
|
70
|
+
|
|
71
|
+
formatted_category = '[BOLD:' + category.replace('\n', ']\n[BOLD:') + ']'
|
|
72
|
+
colour.write(formatted_category)
|
|
73
|
+
|
|
74
|
+
for entry in sorted(report[category]):
|
|
75
|
+
colour.write(f' {entry}')
|
|
76
|
+
|
|
77
|
+
# Summarise the results
|
|
78
|
+
|
|
79
|
+
error_count = terraform.get('error_count', 0)
|
|
80
|
+
warning_count = terraform.get('warning_count', 0)
|
|
81
|
+
|
|
82
|
+
colour.write()
|
|
83
|
+
colour.write(f'[BOLD:Summary:] [BLUE:{error_count}] [BOLD:errors and] [BLUE:{warning_count}] [BOLD:warnings]')
|
|
84
|
+
|
|
85
|
+
################################################################################
|
|
86
|
+
|
|
87
|
+
def tfparse():
|
|
88
|
+
"""Entry point"""
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
main()
|
|
92
|
+
|
|
93
|
+
except KeyboardInterrupt:
|
|
94
|
+
sys.exit(1)
|
|
95
|
+
except BrokenPipeError:
|
|
96
|
+
sys.exit(2)
|
|
97
|
+
|
|
98
|
+
################################################################################
|
|
99
|
+
|
|
100
|
+
if __name__ == '__main__':
|
|
101
|
+
tfparse()
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
#! /usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
################################################################################
|
|
4
|
+
""" Colour colour output
|
|
5
|
+
|
|
6
|
+
Copyright (C) 2017-18 John Skilleter
|
|
7
|
+
|
|
8
|
+
Licence: GPL v3 or later
|
|
9
|
+
|
|
10
|
+
0-15 are the standard VGA colour codes
|
|
11
|
+
16-21 are a few extras
|
|
12
|
+
22-231 appear to be a sort of colour cube
|
|
13
|
+
232-255 are 24 shades of grey
|
|
14
|
+
"""
|
|
15
|
+
################################################################################
|
|
16
|
+
|
|
17
|
+
import sys
|
|
18
|
+
import re
|
|
19
|
+
|
|
20
|
+
################################################################################
|
|
21
|
+
# Constants
|
|
22
|
+
|
|
23
|
+
_ANSI_NORMAL = '\x1b[0m'
|
|
24
|
+
_ANSI_BOLD = '\x1b[1m'
|
|
25
|
+
_ANSI_UNDERSCORE = '\x1b[4m'
|
|
26
|
+
_ANSI_BLINK = '\x1b[5m'
|
|
27
|
+
_ANSI_REVERSE = '\x1b[7m'
|
|
28
|
+
_ANSI_BLACK = '\x1b[30m'
|
|
29
|
+
_ANSI_RED = '\x1b[31m'
|
|
30
|
+
_ANSI_GREEN = '\x1b[32m'
|
|
31
|
+
_ANSI_YELLOW = '\x1b[33m'
|
|
32
|
+
_ANSI_BLUE = '\x1b[34m'
|
|
33
|
+
_ANSI_MAGENTA = '\x1b[35m'
|
|
34
|
+
_ANSI_CYAN = '\x1b[36m'
|
|
35
|
+
_ANSI_WHITE = '\x1b[37m'
|
|
36
|
+
_ANSI_BBLACK = '\x1b[40m'
|
|
37
|
+
_ANSI_BRED = '\x1b[41m'
|
|
38
|
+
_ANSI_BGREEN = '\x1b[42m'
|
|
39
|
+
_ANSI_BYELLOW = '\x1b[43m'
|
|
40
|
+
_ANSI_BBLUE = '\x1b[44m'
|
|
41
|
+
_ANSI_BMAGENTA = '\x1b[45m'
|
|
42
|
+
_ANSI_BCYAN = '\x1b[46m'
|
|
43
|
+
_ANSI_BWHITE = '\x1b[47m'
|
|
44
|
+
|
|
45
|
+
# Looking up tables for converting textual colour codes to ANSI codes
|
|
46
|
+
|
|
47
|
+
ANSI_REGEXES = \
|
|
48
|
+
(
|
|
49
|
+
(r'\[NORMAL:(.*?)\]', r'%s\1%s' % (_ANSI_NORMAL, _ANSI_NORMAL)),
|
|
50
|
+
(r'\[BOLD:(.*?)\]', r'%s\1%s' % (_ANSI_BOLD, _ANSI_NORMAL)),
|
|
51
|
+
(r'\[UNDERSCORE:(.*?)\]', r'%s\1%s' % (_ANSI_UNDERSCORE, _ANSI_NORMAL)),
|
|
52
|
+
(r'\[BLINK:(.*?)\]', r'%s\1%s' % (_ANSI_BLINK, _ANSI_NORMAL)),
|
|
53
|
+
(r'\[REVERSE:(.*?)\]', r'%s\1%s' % (_ANSI_REVERSE, _ANSI_NORMAL)),
|
|
54
|
+
|
|
55
|
+
(r'\[BLACK:(.*?)\]', r'%s\1%s' % (_ANSI_BLACK, _ANSI_NORMAL)),
|
|
56
|
+
(r'\[RED:(.*?)\]', r'%s\1%s' % (_ANSI_RED, _ANSI_NORMAL)),
|
|
57
|
+
(r'\[GREEN:(.*?)\]', r'%s\1%s' % (_ANSI_GREEN, _ANSI_NORMAL)),
|
|
58
|
+
(r'\[YELLOW:(.*?)\]', r'%s\1%s' % (_ANSI_YELLOW, _ANSI_NORMAL)),
|
|
59
|
+
(r'\[BLUE:(.*?)\]', r'%s\1%s' % (_ANSI_BLUE, _ANSI_NORMAL)),
|
|
60
|
+
(r'\[MAGENTA:(.*?)\]', r'%s\1%s' % (_ANSI_MAGENTA, _ANSI_NORMAL)),
|
|
61
|
+
(r'\[CYAN:(.*?)\]', r'%s\1%s' % (_ANSI_CYAN, _ANSI_NORMAL)),
|
|
62
|
+
(r'\[WHITE:(.*?)\]', r'%s\1%s' % (_ANSI_WHITE, _ANSI_NORMAL)),
|
|
63
|
+
|
|
64
|
+
(r'\[BBLACK:(.*?)\]', r'%s\1%s' % (_ANSI_BBLACK, _ANSI_NORMAL)),
|
|
65
|
+
(r'\[BRED:(.*?)\]', r'%s\1%s' % (_ANSI_BRED, _ANSI_NORMAL)),
|
|
66
|
+
(r'\[BGREEN:(.*?)\]', r'%s\1%s' % (_ANSI_BGREEN, _ANSI_NORMAL)),
|
|
67
|
+
(r'\[BYELLOW:(.*?)\]', r'%s\1%s' % (_ANSI_BYELLOW, _ANSI_NORMAL)),
|
|
68
|
+
(r'\[BBLUE:(.*?)\]', r'%s\1%s' % (_ANSI_BBLUE, _ANSI_NORMAL)),
|
|
69
|
+
(r'\[BMAGENTA:(.*?)\]', r'%s\1%s' % (_ANSI_BMAGENTA, _ANSI_NORMAL)),
|
|
70
|
+
(r'\[BCYAN:(.*?)\]', r'%s\1%s' % (_ANSI_BCYAN, _ANSI_NORMAL)),
|
|
71
|
+
(r'\[BWHITE:(.*?)\]', r'%s\1%s' % (_ANSI_BWHITE, _ANSI_NORMAL))
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
ANSI_CODES = \
|
|
75
|
+
(
|
|
76
|
+
('[NORMAL]', _ANSI_NORMAL),
|
|
77
|
+
('[BOLD]', _ANSI_BOLD),
|
|
78
|
+
('[UNDERSCORE]', _ANSI_UNDERSCORE),
|
|
79
|
+
('[BLINK]', _ANSI_BLINK),
|
|
80
|
+
('[REVERSE]', _ANSI_REVERSE),
|
|
81
|
+
|
|
82
|
+
('[BLACK]', _ANSI_BLACK),
|
|
83
|
+
('[RED]', _ANSI_RED),
|
|
84
|
+
('[GREEN]', _ANSI_GREEN),
|
|
85
|
+
('[YELLOW]', _ANSI_YELLOW),
|
|
86
|
+
('[BLUE]', _ANSI_BLUE),
|
|
87
|
+
('[MAGENTA]', _ANSI_MAGENTA),
|
|
88
|
+
('[CYAN]', _ANSI_CYAN),
|
|
89
|
+
('[WHITE]', _ANSI_WHITE),
|
|
90
|
+
|
|
91
|
+
('[BBLACK]', _ANSI_BBLACK),
|
|
92
|
+
('[BRED]', _ANSI_BRED),
|
|
93
|
+
('[BGREEN]', _ANSI_BGREEN),
|
|
94
|
+
('[BYELLOW]', _ANSI_BYELLOW),
|
|
95
|
+
('[BBLUE]', _ANSI_BBLUE),
|
|
96
|
+
('[BMAGENTA]', _ANSI_BMAGENTA),
|
|
97
|
+
('[BCYAN]', _ANSI_BCYAN),
|
|
98
|
+
('[BWHITE]', _ANSI_BWHITE),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Regex to match an ANSI control sequence
|
|
102
|
+
|
|
103
|
+
RE_ANSI = re.compile(r'\x1b\[([0-9][0-9;]*)*m')
|
|
104
|
+
|
|
105
|
+
################################################################################
|
|
106
|
+
|
|
107
|
+
def format(txt):
|
|
108
|
+
""" Convert textual colour codes in a string to ANSI codes.
|
|
109
|
+
Codes can be specified as either [COLOUR], where all following text
|
|
110
|
+
is output in the specified colour or [COLOR:text] where only 'text' is
|
|
111
|
+
output in the colour, with subsequent text output in the default colours """
|
|
112
|
+
|
|
113
|
+
# Replace [COLOUR:text] with COLOURtextNORMAL using regexes
|
|
114
|
+
|
|
115
|
+
if re.search(r'\[.*:.*\]', txt):
|
|
116
|
+
for regex in ANSI_REGEXES:
|
|
117
|
+
txt = re.sub(regex[0], regex[1], txt)
|
|
118
|
+
|
|
119
|
+
# Replace [COLOUR] with COLOUR
|
|
120
|
+
|
|
121
|
+
if re.search(r'\[.*\]', txt):
|
|
122
|
+
for code in ANSI_CODES:
|
|
123
|
+
txt = txt.replace(code[0], code[1])
|
|
124
|
+
|
|
125
|
+
# Now replace [N(N)(N)] with 256 colour colour code.
|
|
126
|
+
|
|
127
|
+
while True:
|
|
128
|
+
p = re.match(r'.*\[([0-9]{1,3})\].*', txt)
|
|
129
|
+
if p is None:
|
|
130
|
+
break
|
|
131
|
+
|
|
132
|
+
value = int(p.group(1))
|
|
133
|
+
txt = txt.replace('[%s]' % p.group(1), '\x1b[38;5;%dm' % value)
|
|
134
|
+
|
|
135
|
+
while True:
|
|
136
|
+
p = re.match(r'.*\[B([0-9]{1,3})\].*', txt)
|
|
137
|
+
if p is None:
|
|
138
|
+
break
|
|
139
|
+
|
|
140
|
+
value = int(p.group(1))
|
|
141
|
+
txt = txt.replace('[B%s]' % p.group(1), '\x1b[48;5;%dm' % value)
|
|
142
|
+
|
|
143
|
+
return txt
|
|
144
|
+
|
|
145
|
+
################################################################################
|
|
146
|
+
|
|
147
|
+
def write(txt=None, newline=True, stream=sys.stdout, indent=0):
|
|
148
|
+
""" Write to the specified stream (defaulting to stdout), converting colour codes to ANSI
|
|
149
|
+
txt can be None, a string or a list of strings."""
|
|
150
|
+
|
|
151
|
+
if txt:
|
|
152
|
+
if isinstance(txt, str):
|
|
153
|
+
txt = txt.split('\n')
|
|
154
|
+
|
|
155
|
+
for n, line in enumerate(txt):
|
|
156
|
+
line = format(line)
|
|
157
|
+
|
|
158
|
+
if indent:
|
|
159
|
+
stream.write(' ' * indent)
|
|
160
|
+
|
|
161
|
+
stream.write(line)
|
|
162
|
+
|
|
163
|
+
if newline or n < len(txt) - 1:
|
|
164
|
+
stream.write('\n')
|
|
165
|
+
elif newline:
|
|
166
|
+
stream.write('\n')
|
|
167
|
+
|
|
168
|
+
################################################################################
|
|
169
|
+
|
|
170
|
+
def error(txt, newline=True, stream=sys.stderr, status=1):
|
|
171
|
+
""" Write an error message to the specified stream (defaulting to
|
|
172
|
+
stderr) and exit with the specified status code (defaulting to 1) """
|
|
173
|
+
|
|
174
|
+
write(txt, newline, stream)
|
|
175
|
+
|
|
176
|
+
sys.exit(status)
|
|
177
|
+
|
|
178
|
+
################################################################################
|
|
179
|
+
|
|
180
|
+
if __name__ == '__main__':
|
|
181
|
+
write('Foreground: [RED]red [GREEN]green [BLACK]black [NORMAL]normal')
|
|
182
|
+
write('Background: [BRED]red [BGREEN]green [BBLACK]black [NORMAL]normal')
|
|
183
|
+
|
|
184
|
+
write('Foreground: [BBLUE:blue] [RED:red] normal')
|
|
185
|
+
|
|
186
|
+
for combo in (0, 1, 2):
|
|
187
|
+
print()
|
|
188
|
+
if combo == 0:
|
|
189
|
+
print('Background colours')
|
|
190
|
+
elif combo == 1:
|
|
191
|
+
print('Foreground colours')
|
|
192
|
+
else:
|
|
193
|
+
print('Combinations')
|
|
194
|
+
|
|
195
|
+
print()
|
|
196
|
+
for y in range(0, 32):
|
|
197
|
+
for x in range(0, 8):
|
|
198
|
+
colour = x + y * 8
|
|
199
|
+
|
|
200
|
+
if combo == 0:
|
|
201
|
+
write(format('[B%d] %04d ' % (colour, colour)), newline=False)
|
|
202
|
+
elif combo == 1:
|
|
203
|
+
write(format('[%d] %04d ' % (colour, colour)), newline=False)
|
|
204
|
+
else:
|
|
205
|
+
write(format('[B%d] %04d [%d] %04d ' % (colour, colour, 255 - colour, 255 - colour)), newline=False)
|
|
206
|
+
|
|
207
|
+
write('[NORMAL]')
|
|
208
|
+
|
|
209
|
+
print()
|
|
210
|
+
|
|
211
|
+
error('Error message (nothing should be output after this)', status=0)
|
|
212
|
+
|
|
213
|
+
write('This message should not appear')
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
#! /usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
""" Convert colour highlighting codes from the LS_COLORS environment variable
|
|
4
|
+
used by ls to curses
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
################################################################################
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
import os
|
|
11
|
+
import glob
|
|
12
|
+
import fnmatch
|
|
13
|
+
import curses
|
|
14
|
+
import stat
|
|
15
|
+
|
|
16
|
+
################################################################################
|
|
17
|
+
|
|
18
|
+
class CursesDircolors:
|
|
19
|
+
""" Convert dircolors codes to curses colours """
|
|
20
|
+
|
|
21
|
+
# Convert standard foreground and background codes to curses equivalents
|
|
22
|
+
|
|
23
|
+
ANSI_CONVERT_FORE = {
|
|
24
|
+
30: curses.COLOR_BLACK,
|
|
25
|
+
31: curses.COLOR_RED,
|
|
26
|
+
32: curses.COLOR_GREEN,
|
|
27
|
+
33: curses.COLOR_YELLOW,
|
|
28
|
+
34: curses.COLOR_BLUE,
|
|
29
|
+
35: curses.COLOR_MAGENTA,
|
|
30
|
+
36: curses.COLOR_CYAN,
|
|
31
|
+
37: curses.COLOR_WHITE,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
ANSI_CONVERT_BACK = {
|
|
35
|
+
40: curses.COLOR_BLACK,
|
|
36
|
+
41: curses.COLOR_RED,
|
|
37
|
+
42: curses.COLOR_GREEN,
|
|
38
|
+
43: curses.COLOR_YELLOW,
|
|
39
|
+
44: curses.COLOR_BLUE,
|
|
40
|
+
45: curses.COLOR_MAGENTA,
|
|
41
|
+
46: curses.COLOR_CYAN,
|
|
42
|
+
47: curses.COLOR_WHITE,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# Convert attribute codes to their meanings
|
|
46
|
+
# TODO: Attributes not handled yet
|
|
47
|
+
|
|
48
|
+
ANSI_CONVERT_ATTR = {
|
|
49
|
+
0: 0,
|
|
50
|
+
1: curses.A_BOLD,
|
|
51
|
+
4: curses.A_UNDERLINE,
|
|
52
|
+
5: curses.A_BLINK,
|
|
53
|
+
7: curses.A_BLINK,
|
|
54
|
+
8: curses.A_INVIS,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# Default colour
|
|
58
|
+
|
|
59
|
+
DEFAULT_ATTR = {'attr': [], 'fore': -1, 'back': -1}
|
|
60
|
+
|
|
61
|
+
################################################################################
|
|
62
|
+
|
|
63
|
+
def __init__(self, reserved=0):
|
|
64
|
+
# Create the lookup tables associating special type codes or wildcards
|
|
65
|
+
# with colour pairs.
|
|
66
|
+
|
|
67
|
+
self.colour_pairs = [[-1, -1]]
|
|
68
|
+
|
|
69
|
+
self.wildcard_highlight = {}
|
|
70
|
+
self.special_highlight = {}
|
|
71
|
+
|
|
72
|
+
self.reserved = reserved
|
|
73
|
+
|
|
74
|
+
self.init_ls_colours()
|
|
75
|
+
|
|
76
|
+
################################################################################
|
|
77
|
+
|
|
78
|
+
def curses_alloc_pair(self, attr):
|
|
79
|
+
""" Given a set of attributes return the equivalent curses colour pair,
|
|
80
|
+
creating a new one if a matching one doesn't already exsit """
|
|
81
|
+
|
|
82
|
+
# TODO: Take account of attributes as well as colours
|
|
83
|
+
|
|
84
|
+
colours = [attr['fore'], attr['back']]
|
|
85
|
+
|
|
86
|
+
# Get an existing colour pair that uses the same colours or create
|
|
87
|
+
# a new one if one doesn't exist
|
|
88
|
+
|
|
89
|
+
if colours in self.colour_pairs:
|
|
90
|
+
pair_index = self.colour_pairs.index(colours) + self.reserved
|
|
91
|
+
else:
|
|
92
|
+
pair_index = len(self.colour_pairs) + self.reserved
|
|
93
|
+
self.colour_pairs.append(colours)
|
|
94
|
+
curses.init_pair(pair_index, attr['fore'], attr['back'])
|
|
95
|
+
|
|
96
|
+
return pair_index
|
|
97
|
+
|
|
98
|
+
################################################################################
|
|
99
|
+
|
|
100
|
+
def curses_colour(self, code):
|
|
101
|
+
""" Return a cursors colour pair index for the specified dircolor colour
|
|
102
|
+
code string. """
|
|
103
|
+
|
|
104
|
+
# Default attribute
|
|
105
|
+
|
|
106
|
+
attr = {'attr': [], 'fore': -1, 'back': -1}
|
|
107
|
+
|
|
108
|
+
# Non-zero if processing multi-value colour code
|
|
109
|
+
|
|
110
|
+
special = 0
|
|
111
|
+
special_item = None
|
|
112
|
+
|
|
113
|
+
# We trigger a ValueError and fail on anything that's wrong in the code
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
# Split into fields and convert to integer values
|
|
117
|
+
|
|
118
|
+
codes = [int(c) for c in code.split(';')]
|
|
119
|
+
|
|
120
|
+
for entry in codes:
|
|
121
|
+
# Process 2nd entry in a special colour sequence - must have value of 5
|
|
122
|
+
|
|
123
|
+
if special == 1:
|
|
124
|
+
if entry != 5:
|
|
125
|
+
raise ValueError
|
|
126
|
+
special = 2
|
|
127
|
+
|
|
128
|
+
# Process 3rd entry in a special colour sequence - must be the colour
|
|
129
|
+
# code between 0 and 255
|
|
130
|
+
|
|
131
|
+
elif special == 2:
|
|
132
|
+
if entry < 0 or entry > 255:
|
|
133
|
+
raise ValueError
|
|
134
|
+
|
|
135
|
+
attr[special_item] = entry
|
|
136
|
+
special = 0
|
|
137
|
+
|
|
138
|
+
# Normal foreground colour
|
|
139
|
+
|
|
140
|
+
elif entry in self.ANSI_CONVERT_FORE:
|
|
141
|
+
attr['fore'] = self.ANSI_CONVERT_FORE[entry]
|
|
142
|
+
|
|
143
|
+
# Normal background colour
|
|
144
|
+
|
|
145
|
+
elif entry in self.ANSI_CONVERT_BACK:
|
|
146
|
+
attr['back'] = self.ANSI_CONVERT_BACK[entry]
|
|
147
|
+
|
|
148
|
+
# Special foreground colour in the form 38;5;VALUE
|
|
149
|
+
|
|
150
|
+
elif entry == 38:
|
|
151
|
+
special = 1
|
|
152
|
+
special_item = 'fore'
|
|
153
|
+
|
|
154
|
+
# Special background colour in the form 48;5;VALUE
|
|
155
|
+
|
|
156
|
+
elif entry == 48:
|
|
157
|
+
special = 1
|
|
158
|
+
special_item = 'back'
|
|
159
|
+
|
|
160
|
+
# Attribute (underline, bold, etc.)
|
|
161
|
+
|
|
162
|
+
elif entry in self.ANSI_CONVERT_ATTR:
|
|
163
|
+
attr['attr'].append(self.ANSI_CONVERT_ATTR[entry])
|
|
164
|
+
|
|
165
|
+
# Anything else is an error
|
|
166
|
+
|
|
167
|
+
else:
|
|
168
|
+
raise ValueError
|
|
169
|
+
|
|
170
|
+
except ValueError:
|
|
171
|
+
print(f'Invalid colour specification: "{code}"')
|
|
172
|
+
sys.exit(1)
|
|
173
|
+
|
|
174
|
+
# Allocate a colour pair for the colour combination and return it
|
|
175
|
+
|
|
176
|
+
return self.curses_alloc_pair(attr)
|
|
177
|
+
|
|
178
|
+
################################################################################
|
|
179
|
+
|
|
180
|
+
def init_ls_colours(self):
|
|
181
|
+
""" Generate tables matching special file types (fifos, sockets, etc.) and
|
|
182
|
+
wildcards to curses colour pairs """
|
|
183
|
+
|
|
184
|
+
colour_data = os.environ.get('LS_COLORS', '').split(':')
|
|
185
|
+
|
|
186
|
+
# Iterate through the highlighters, create/get a colour pair corresponding
|
|
187
|
+
# to the colour codes and save one of the tables.
|
|
188
|
+
|
|
189
|
+
for item in colour_data:
|
|
190
|
+
item = item.strip()
|
|
191
|
+
if '=' in item:
|
|
192
|
+
code, colour = item.split('=')
|
|
193
|
+
|
|
194
|
+
colour_pair = self.curses_colour(colour)
|
|
195
|
+
|
|
196
|
+
if len(code) == 2 and '*' not in code and '.' not in code:
|
|
197
|
+
self.special_highlight[code] = colour_pair
|
|
198
|
+
else:
|
|
199
|
+
self.wildcard_highlight[code] = colour_pair
|
|
200
|
+
|
|
201
|
+
################################################################################
|
|
202
|
+
|
|
203
|
+
def get_colour(self, filename, filemode=None):
|
|
204
|
+
""" Get the curses colour for a filename, returns 0 if no highlighting
|
|
205
|
+
is needed """
|
|
206
|
+
|
|
207
|
+
if filemode:
|
|
208
|
+
if stat.S_ISDIR(filemode):
|
|
209
|
+
if 'di' in self.special_highlight:
|
|
210
|
+
return self.special_highlight['di']
|
|
211
|
+
elif stat.S_ISLNK(filemode):
|
|
212
|
+
destfile = os.readlink(filename)
|
|
213
|
+
|
|
214
|
+
if os.path.exists(destfile):
|
|
215
|
+
if 'ln' in self.special_highlight:
|
|
216
|
+
return self.special_highlight['ln']
|
|
217
|
+
elif 'or' in self.special_highlight:
|
|
218
|
+
return self.special_highlight['or']
|
|
219
|
+
|
|
220
|
+
elif stat.S_ISBLK(filemode):
|
|
221
|
+
if 'bd' in self.special_highlight:
|
|
222
|
+
return self.special_highlight['bd']
|
|
223
|
+
elif stat.S_ISCHR(filemode):
|
|
224
|
+
if 'cd' in self.special_highlight:
|
|
225
|
+
return self.special_highlight['cd']
|
|
226
|
+
|
|
227
|
+
if filemode & stat.S_IXUSR:
|
|
228
|
+
if 'ex' in self.special_highlight:
|
|
229
|
+
return self.special_highlight['ex']
|
|
230
|
+
|
|
231
|
+
for entry in self.wildcard_highlight:
|
|
232
|
+
if fnmatch.fnmatch(filename, entry):
|
|
233
|
+
colour = self.wildcard_highlight[entry]
|
|
234
|
+
break
|
|
235
|
+
else:
|
|
236
|
+
colour = 0
|
|
237
|
+
|
|
238
|
+
return colour
|
|
239
|
+
|
|
240
|
+
################################################################################
|
|
241
|
+
|
|
242
|
+
def get_colour_pair(self, filename, filemode=None):
|
|
243
|
+
""" Get the curses colour pair for a filename, optionally specifying the
|
|
244
|
+
file mode (as per os.stat()) """
|
|
245
|
+
|
|
246
|
+
return curses.color_pair(self.get_colour(filename, filemode))
|
|
247
|
+
|
|
248
|
+
################################################################################
|
|
249
|
+
|
|
250
|
+
def _test_code(stdscr):
|
|
251
|
+
""" Entry point """
|
|
252
|
+
|
|
253
|
+
curses.start_color()
|
|
254
|
+
curses.use_default_colors()
|
|
255
|
+
|
|
256
|
+
# Initialise colours
|
|
257
|
+
|
|
258
|
+
dc = CursesDircolors()
|
|
259
|
+
|
|
260
|
+
# Demo code to list files specified by the first command line argument
|
|
261
|
+
# highlighted appropriately
|
|
262
|
+
|
|
263
|
+
y = 0
|
|
264
|
+
for filename in glob.glob(sys.argv[1]):
|
|
265
|
+
colour = dc.get_colour(filename)
|
|
266
|
+
|
|
267
|
+
stdscr.addstr(y, 0, filename, curses.color_pair(colour))
|
|
268
|
+
|
|
269
|
+
y += 1
|
|
270
|
+
if y > 30:
|
|
271
|
+
break
|
|
272
|
+
|
|
273
|
+
stdscr.getch()
|
|
274
|
+
|
|
275
|
+
################################################################################
|
|
276
|
+
|
|
277
|
+
if __name__ == "__main__":
|
|
278
|
+
curses.wrapper(_test_code)
|