skilleter-thingy 0.0.22__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 +0 -0
- skilleter_thingy/addpath.py +107 -0
- skilleter_thingy/aws.py +548 -0
- skilleter_thingy/borger.py +269 -0
- skilleter_thingy/colour.py +213 -0
- skilleter_thingy/console_colours.py +63 -0
- skilleter_thingy/dc_curses.py +278 -0
- skilleter_thingy/dc_defaults.py +221 -0
- skilleter_thingy/dc_util.py +50 -0
- skilleter_thingy/dircolors.py +308 -0
- skilleter_thingy/diskspacecheck.py +67 -0
- skilleter_thingy/docker.py +95 -0
- skilleter_thingy/docker_purge.py +113 -0
- skilleter_thingy/ffind.py +536 -0
- skilleter_thingy/files.py +142 -0
- skilleter_thingy/ggit.py +90 -0
- skilleter_thingy/ggrep.py +154 -0
- skilleter_thingy/git.py +1368 -0
- skilleter_thingy/git2.py +1307 -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 +1416 -0
- skilleter_thingy/git_update.py +385 -0
- skilleter_thingy/git_wt.py +96 -0
- skilleter_thingy/gitcmp_helper.py +322 -0
- skilleter_thingy/gitlab.py +193 -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/logger.py +112 -0
- skilleter_thingy/moviemover.py +133 -0
- skilleter_thingy/path.py +156 -0
- skilleter_thingy/photodupe.py +110 -0
- skilleter_thingy/phototidier.py +248 -0
- skilleter_thingy/popup.py +87 -0
- skilleter_thingy/process.py +112 -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/run.py +334 -0
- skilleter_thingy/s3_sync.py +383 -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/tfm_pane.py +595 -0
- skilleter_thingy/tfparse.py +101 -0
- skilleter_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.22.dist-info/LICENSE +619 -0
- skilleter_thingy-0.0.22.dist-info/METADATA +22 -0
- skilleter_thingy-0.0.22.dist-info/RECORD +67 -0
- skilleter_thingy-0.0.22.dist-info/WHEEL +5 -0
- skilleter_thingy-0.0.22.dist-info/entry_points.txt +43 -0
- skilleter_thingy-0.0.22.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
#! /usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Wrapper for the borg backup command
|
|
5
|
+
|
|
6
|
+
Copyright (C) 2018 John Skilleter
|
|
7
|
+
|
|
8
|
+
TODO: Major tidy-up as this is a translation of a Bash script.
|
|
9
|
+
TODO: Merge with the usb-backup script since both do almost the same job
|
|
10
|
+
TODO: Default configuration file should be named for the hostname
|
|
11
|
+
TODO: Move all configuration data into the configuration file
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
################################################################################
|
|
15
|
+
# Imports
|
|
16
|
+
|
|
17
|
+
import sys
|
|
18
|
+
import os
|
|
19
|
+
import time
|
|
20
|
+
import argparse
|
|
21
|
+
import configparser
|
|
22
|
+
import subprocess
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
################################################################################
|
|
26
|
+
# Variables
|
|
27
|
+
|
|
28
|
+
DEFAULT_CONFIG_FILE = Path('borger.ini')
|
|
29
|
+
|
|
30
|
+
COMMANDS = ('backup', 'mount', 'umount', 'compact', 'info', 'prune', 'check', 'init')
|
|
31
|
+
|
|
32
|
+
# TODO: NOT USED
|
|
33
|
+
PRUNE_OPTIONS = [
|
|
34
|
+
'--keep-within', '7d',
|
|
35
|
+
'--keep-daily', '30',
|
|
36
|
+
'--keep-weekly', '26',
|
|
37
|
+
'--keep-monthly', '24',
|
|
38
|
+
'--keep-yearly', '10',
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
################################################################################
|
|
42
|
+
|
|
43
|
+
def run(args, cmd):
|
|
44
|
+
"""Run a subprocess."""
|
|
45
|
+
|
|
46
|
+
if args.debug:
|
|
47
|
+
cmd_str = ' '.join(cmd)
|
|
48
|
+
print(f'Running "{cmd_str}"')
|
|
49
|
+
|
|
50
|
+
return subprocess.run(cmd, check=True)
|
|
51
|
+
|
|
52
|
+
################################################################################
|
|
53
|
+
|
|
54
|
+
def borg_backup(args, exclude_list):
|
|
55
|
+
"""Perform a backup."""
|
|
56
|
+
|
|
57
|
+
create_options = ['--compression', 'auto,lzma']
|
|
58
|
+
|
|
59
|
+
version = time.strftime('%Y-%m-%d-%H:%M:%S')
|
|
60
|
+
|
|
61
|
+
print(f'Creating backup version {version}')
|
|
62
|
+
|
|
63
|
+
if args.verbose:
|
|
64
|
+
create_options += ['--list', '--filter=AMC']
|
|
65
|
+
|
|
66
|
+
if args.dryrun:
|
|
67
|
+
create_options.append('--dry-run')
|
|
68
|
+
else:
|
|
69
|
+
create_options.append('--stats')
|
|
70
|
+
|
|
71
|
+
exclude_opts = []
|
|
72
|
+
|
|
73
|
+
if exclude_list:
|
|
74
|
+
for exclude in exclude_list:
|
|
75
|
+
exclude_opts += ['--exclude', exclude]
|
|
76
|
+
|
|
77
|
+
os.chdir(args.source)
|
|
78
|
+
|
|
79
|
+
run(args,
|
|
80
|
+
['borg'] + args.options + ['create', f'{str(args.destination)}::{version}', str(args.source)] + create_options +
|
|
81
|
+
['--show-rc', '--one-file-system', '--exclude-caches'] + exclude_opts)
|
|
82
|
+
|
|
83
|
+
################################################################################
|
|
84
|
+
|
|
85
|
+
def borg_prune(args):
|
|
86
|
+
"""Prune the repo by limiting the number of backups stored."""
|
|
87
|
+
|
|
88
|
+
print('Pruning old backups')
|
|
89
|
+
|
|
90
|
+
# Keep all backups for at least 7 days, 1 per day for 30 days, 1 per week for 2 years
|
|
91
|
+
# 1 per month for 4 years and 1 per year for 10 years.
|
|
92
|
+
|
|
93
|
+
run(args, ['borg'] + args.options + ['prune', str(args.destination)] + PRUNE_OPTIONS)
|
|
94
|
+
|
|
95
|
+
################################################################################
|
|
96
|
+
|
|
97
|
+
def borg_compact(args):
|
|
98
|
+
"""Compact the repo."""
|
|
99
|
+
|
|
100
|
+
print('Compacting the backup')
|
|
101
|
+
|
|
102
|
+
# Keep all backups for at least 7 days, 1 per day for 30 days, 1 per week for 2 years
|
|
103
|
+
# 1 per month for 4 years and 1 per year for 10 years.
|
|
104
|
+
|
|
105
|
+
run(args, ['borg'] + args.options + ['compact', str(args.destination)])
|
|
106
|
+
|
|
107
|
+
################################################################################
|
|
108
|
+
|
|
109
|
+
def borg_info(args):
|
|
110
|
+
"""Info."""
|
|
111
|
+
|
|
112
|
+
run(args, ['borg'] + args.options + ['info', str(args.destination)])
|
|
113
|
+
|
|
114
|
+
################################################################################
|
|
115
|
+
|
|
116
|
+
def borg_mount(args):
|
|
117
|
+
"""Mount."""
|
|
118
|
+
|
|
119
|
+
print(f'Mounting Borg backups at {args.mount_dir}')
|
|
120
|
+
|
|
121
|
+
mount = Path(args.mount_dir)
|
|
122
|
+
|
|
123
|
+
if not mount.is_dir():
|
|
124
|
+
mount.mkdir()
|
|
125
|
+
|
|
126
|
+
run(args, ['borg'] + args.options + ['mount', str(args.destination), str(mount)])
|
|
127
|
+
|
|
128
|
+
################################################################################
|
|
129
|
+
|
|
130
|
+
def borg_umount(args):
|
|
131
|
+
"""Unmount."""
|
|
132
|
+
|
|
133
|
+
print('Unmounting {args.mount}')
|
|
134
|
+
|
|
135
|
+
run(args, ['borg'] + args.options + ['umount', str(args.mount)])
|
|
136
|
+
|
|
137
|
+
################################################################################
|
|
138
|
+
|
|
139
|
+
def borg_check(args):
|
|
140
|
+
"""Check the status of a backup."""
|
|
141
|
+
|
|
142
|
+
run(args, ['borg'] + args.options + ['check', str(args.destination)])
|
|
143
|
+
|
|
144
|
+
################################################################################
|
|
145
|
+
|
|
146
|
+
def borg_init(args):
|
|
147
|
+
"""Initialise a backup."""
|
|
148
|
+
|
|
149
|
+
run(args, ['borg'] + args.options + ['init', str(args.destination), '--encryption=none'])
|
|
150
|
+
|
|
151
|
+
################################################################################
|
|
152
|
+
|
|
153
|
+
def process_excludes(exclude_data):
|
|
154
|
+
"""Process the include list from the configuration file."""
|
|
155
|
+
|
|
156
|
+
return exclude_data.replace('%', str(Path.cwd())).split('\n')
|
|
157
|
+
|
|
158
|
+
################################################################################
|
|
159
|
+
|
|
160
|
+
def main():
|
|
161
|
+
"""Entry point."""
|
|
162
|
+
|
|
163
|
+
command_list = ', '.join(COMMANDS)
|
|
164
|
+
|
|
165
|
+
parser = argparse.ArgumentParser(description='Wrapper app for Borg backup to make it easier to use')
|
|
166
|
+
parser.add_argument('--dryrun', '--dry-run', '-D', action='store_true', help='Dry-run comands')
|
|
167
|
+
parser.add_argument('--debug', '-d', action='store_true', help='Debug')
|
|
168
|
+
parser.add_argument('--verbose', '-v', action='store_true', help='Verbosity to the maximum')
|
|
169
|
+
parser.add_argument('--config', '-c', default=None, help='Specify the configuration file')
|
|
170
|
+
parser.add_argument('commands', nargs='+', help=f'One or more commands ({command_list})')
|
|
171
|
+
args = parser.parse_args()
|
|
172
|
+
|
|
173
|
+
# If no config file specified then look in all the usual places
|
|
174
|
+
|
|
175
|
+
if args.config:
|
|
176
|
+
args.config = Path(args.config)
|
|
177
|
+
elif DEFAULT_CONFIG_FILE.is_file():
|
|
178
|
+
args.config = DEFAULT_CONFIG_FILE
|
|
179
|
+
else:
|
|
180
|
+
args.config = Path.home() / DEFAULT_CONFIG_FILE
|
|
181
|
+
|
|
182
|
+
if not args.config.is_file():
|
|
183
|
+
args.config = Path(sys.argv[0]).parent / DEFAULT_CONFIG_FILE
|
|
184
|
+
|
|
185
|
+
# Check that the configuration file exists
|
|
186
|
+
|
|
187
|
+
if not args.config.is_file():
|
|
188
|
+
print(f'Configuration file "{args.config}" not found')
|
|
189
|
+
sys.exit(1)
|
|
190
|
+
|
|
191
|
+
# Default options
|
|
192
|
+
|
|
193
|
+
args.options = []
|
|
194
|
+
|
|
195
|
+
# Read the configuration file
|
|
196
|
+
|
|
197
|
+
config = configparser.ConfigParser(interpolation=configparser.ExtendedInterpolation())
|
|
198
|
+
config.read(args.config)
|
|
199
|
+
|
|
200
|
+
if 'borger' not in config:
|
|
201
|
+
print('Invalid configuration file "args.config"')
|
|
202
|
+
sys.exit(1)
|
|
203
|
+
|
|
204
|
+
exclude = process_excludes(config['borger']['exclude']) if 'exclude' in config['borger'] else []
|
|
205
|
+
|
|
206
|
+
if 'prune' in config['borger']:
|
|
207
|
+
# TODO: Stuff
|
|
208
|
+
print('Parser for the prune option is not implemented yet')
|
|
209
|
+
sys.exit(1)
|
|
210
|
+
|
|
211
|
+
if 'destination' in config['borger']:
|
|
212
|
+
args.destination = config['borger']['destination']
|
|
213
|
+
else:
|
|
214
|
+
print('Destination directory not specified')
|
|
215
|
+
sys.exit(1)
|
|
216
|
+
|
|
217
|
+
if 'source' in config['borger']:
|
|
218
|
+
args.source = Path(config['borger']['source'])
|
|
219
|
+
else:
|
|
220
|
+
print('Source directory not specified')
|
|
221
|
+
sys.exit(1)
|
|
222
|
+
|
|
223
|
+
# Initialise if necessary
|
|
224
|
+
|
|
225
|
+
if args.debug:
|
|
226
|
+
args.options.append('--verbose')
|
|
227
|
+
|
|
228
|
+
if args.verbose:
|
|
229
|
+
args.options.append('--progress')
|
|
230
|
+
|
|
231
|
+
# Decide what to do
|
|
232
|
+
|
|
233
|
+
for command in args.commands:
|
|
234
|
+
if command == 'backup':
|
|
235
|
+
borg_backup(args, exclude)
|
|
236
|
+
elif command == 'mount':
|
|
237
|
+
borg_mount(args)
|
|
238
|
+
elif command == 'umount':
|
|
239
|
+
borg_umount(args)
|
|
240
|
+
elif command == 'info':
|
|
241
|
+
borg_info(args)
|
|
242
|
+
elif command == 'prune':
|
|
243
|
+
borg_prune(args)
|
|
244
|
+
elif command == 'check':
|
|
245
|
+
borg_check(args)
|
|
246
|
+
elif command == 'init':
|
|
247
|
+
borg_init(args)
|
|
248
|
+
elif command == 'compact':
|
|
249
|
+
borg_compact(args)
|
|
250
|
+
else:
|
|
251
|
+
print(f'Unrecognized command: {command}')
|
|
252
|
+
sys.exit(2)
|
|
253
|
+
|
|
254
|
+
################################################################################
|
|
255
|
+
|
|
256
|
+
def borger():
|
|
257
|
+
"""Entry point"""
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
main()
|
|
261
|
+
except KeyboardInterrupt:
|
|
262
|
+
sys.exit(1)
|
|
263
|
+
except BrokenPipeError:
|
|
264
|
+
sys.exit(2)
|
|
265
|
+
|
|
266
|
+
################################################################################
|
|
267
|
+
|
|
268
|
+
if __name__ == '__main__':
|
|
269
|
+
borger()
|
|
@@ -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,63 @@
|
|
|
1
|
+
#! /usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
################################################################################
|
|
4
|
+
""" Output all console colours
|
|
5
|
+
|
|
6
|
+
Copyright (C) 2017-18 John Skilleter
|
|
7
|
+
|
|
8
|
+
Licence: GPL v3 or later
|
|
9
|
+
"""
|
|
10
|
+
################################################################################
|
|
11
|
+
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
from skilleter_thingy import colour
|
|
15
|
+
|
|
16
|
+
################################################################################
|
|
17
|
+
|
|
18
|
+
def main():
|
|
19
|
+
""" Main function - draw the colour grid """
|
|
20
|
+
|
|
21
|
+
# Extended ANSI colour are slightly weird.
|
|
22
|
+
# Colours 0-15 are the standard basic colours
|
|
23
|
+
# Colours 16-231 form a 6x6x6 colour cube
|
|
24
|
+
# Colours 232-255 are greyscale range with colours 0 and 15 as black and white
|
|
25
|
+
|
|
26
|
+
for code in range(0, 256):
|
|
27
|
+
if code in (8, 16) or (code > 16 and (code - 16) % 6 == 0):
|
|
28
|
+
colour.write('')
|
|
29
|
+
|
|
30
|
+
if (code - 16) % 36 == 0:
|
|
31
|
+
colour.write('')
|
|
32
|
+
|
|
33
|
+
# Set the foreground code to be white for dark backgrounds
|
|
34
|
+
|
|
35
|
+
foreground = 15 if code in (0, 1, 4, 5, 8, 12) \
|
|
36
|
+
or (16 <= code <= 33) \
|
|
37
|
+
or (52 <= code <= 69) \
|
|
38
|
+
or (88 <= code <= 105) \
|
|
39
|
+
or (124 <= code <= 135) \
|
|
40
|
+
or (160 <= code <= 171) \
|
|
41
|
+
or (196 <= code <= 201) \
|
|
42
|
+
or (232 <= code <= 243) else 0
|
|
43
|
+
|
|
44
|
+
colour.write('[B%d][%d] %3d [NORMAL] ' % (code, foreground, code), newline=False)
|
|
45
|
+
|
|
46
|
+
print()
|
|
47
|
+
|
|
48
|
+
################################################################################
|
|
49
|
+
|
|
50
|
+
def console_colours():
|
|
51
|
+
"""Entry point"""
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
main()
|
|
55
|
+
except KeyboardInterrupt:
|
|
56
|
+
sys.exit(1)
|
|
57
|
+
except BrokenPipeError:
|
|
58
|
+
sys.exit(2)
|
|
59
|
+
|
|
60
|
+
################################################################################
|
|
61
|
+
|
|
62
|
+
if __name__ == '__main__':
|
|
63
|
+
console_colours()
|