skilleter-modules 0.0.3__tar.gz → 0.0.6__tar.gz
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.
- {skilleter_modules-0.0.3/src/skilleter_modules.egg-info → skilleter_modules-0.0.6}/PKG-INFO +3 -1
- {skilleter_modules-0.0.3 → skilleter_modules-0.0.6}/pyproject.toml +6 -1
- {skilleter_modules-0.0.3 → skilleter_modules-0.0.6}/src/skilleter_modules/colour.py +0 -54
- {skilleter_modules-0.0.3 → skilleter_modules-0.0.6}/src/skilleter_modules/dc_curses.py +6 -4
- {skilleter_modules-0.0.3 → skilleter_modules-0.0.6}/src/skilleter_modules/dircolors.py +6 -1
- {skilleter_modules-0.0.3 → skilleter_modules-0.0.6}/src/skilleter_modules/docker.py +15 -8
- {skilleter_modules-0.0.3 → skilleter_modules-0.0.6}/src/skilleter_modules/files.py +4 -2
- {skilleter_modules-0.0.3 → skilleter_modules-0.0.6}/src/skilleter_modules/git.py +2 -2
- {skilleter_modules-0.0.3 → skilleter_modules-0.0.6}/src/skilleter_modules/path.py +4 -1
- {skilleter_modules-0.0.3 → skilleter_modules-0.0.6}/src/skilleter_modules/popup.py +5 -3
- {skilleter_modules-0.0.3 → skilleter_modules-0.0.6}/src/skilleter_modules/run.py +4 -3
- {skilleter_modules-0.0.3 → skilleter_modules-0.0.6}/src/skilleter_modules/tidy.py +2 -2
- {skilleter_modules-0.0.3 → skilleter_modules-0.0.6/src/skilleter_modules.egg-info}/PKG-INFO +3 -1
- {skilleter_modules-0.0.3 → skilleter_modules-0.0.6}/src/skilleter_modules.egg-info/SOURCES.txt +12 -2
- skilleter_modules-0.0.6/tests/test_colour.py +65 -0
- skilleter_modules-0.0.6/tests/test_dircolors.py +27 -0
- skilleter_modules-0.0.6/tests/test_docker.py +30 -0
- skilleter_modules-0.0.6/tests/test_files.py +22 -0
- skilleter_modules-0.0.6/tests/test_git.py +6 -0
- skilleter_modules-0.0.6/tests/test_gitlab.py +4 -0
- skilleter_modules-0.0.6/tests/test_path.py +23 -0
- skilleter_modules-0.0.6/tests/test_popup.py +70 -0
- skilleter_modules-0.0.6/tests/test_run.py +32 -0
- skilleter_modules-0.0.6/tests/test_tfm_pane.py +6 -0
- skilleter_modules-0.0.6/tests/test_tidy.py +10 -0
- skilleter_modules-0.0.3/src/skilleter_modules/tfm_pane.py +0 -593
- {skilleter_modules-0.0.3 → skilleter_modules-0.0.6}/LICENSE +0 -0
- {skilleter_modules-0.0.3 → skilleter_modules-0.0.6}/README.md +0 -0
- {skilleter_modules-0.0.3 → skilleter_modules-0.0.6}/setup.cfg +0 -0
- {skilleter_modules-0.0.3 → skilleter_modules-0.0.6}/src/skilleter_modules/__init__.py +0 -0
- {skilleter_modules-0.0.3 → skilleter_modules-0.0.6}/src/skilleter_modules/dc_defaults.py +0 -0
- {skilleter_modules-0.0.3 → skilleter_modules-0.0.6}/src/skilleter_modules/dc_util.py +0 -0
- {skilleter_modules-0.0.3 → skilleter_modules-0.0.6}/src/skilleter_modules/gitlab.py +0 -0
- {skilleter_modules-0.0.3 → skilleter_modules-0.0.6}/src/skilleter_modules/venv_template.py +0 -0
- {skilleter_modules-0.0.3 → skilleter_modules-0.0.6}/src/skilleter_modules.egg-info/dependency_links.txt +0 -0
- {skilleter_modules-0.0.3 → skilleter_modules-0.0.6}/src/skilleter_modules.egg-info/requires.txt +0 -0
- {skilleter_modules-0.0.3 → skilleter_modules-0.0.6}/src/skilleter_modules.egg-info/top_level.txt +0 -0
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: skilleter_modules
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.6
|
|
4
4
|
Summary: Modules used by my various Python projects (and hopefully useful to other people)
|
|
5
5
|
Author-email: John Skilleter <john@skilleter.org.uk>
|
|
6
6
|
Project-URL: Home, https://skilleter.org.uk
|
|
7
|
+
Project-URL: Repository, https://gitlab.com/skilleter/skilleter-modules
|
|
8
|
+
Project-URL: Issues, https://gitlab.com/skilleter/skilleter-extras/-/modules
|
|
7
9
|
Classifier: Programming Language :: Python :: 3
|
|
8
10
|
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
9
11
|
Classifier: Operating System :: OS Independent
|
|
@@ -7,7 +7,7 @@ name = "skilleter_modules"
|
|
|
7
7
|
|
|
8
8
|
# Version must be incremented to install updated skilleter-extras
|
|
9
9
|
|
|
10
|
-
version = "0.0.
|
|
10
|
+
version = "0.0.6"
|
|
11
11
|
|
|
12
12
|
authors = [
|
|
13
13
|
{name="John Skilleter", email="john@skilleter.org.uk"},
|
|
@@ -32,5 +32,10 @@ dependencies = [
|
|
|
32
32
|
|
|
33
33
|
[project.urls]
|
|
34
34
|
Home = "https://skilleter.org.uk"
|
|
35
|
+
Repository = "https://gitlab.com/skilleter/skilleter-modules"
|
|
36
|
+
Issues = "https://gitlab.com/skilleter/skilleter-extras/-/modules"
|
|
35
37
|
|
|
36
38
|
[project.scripts]
|
|
39
|
+
|
|
40
|
+
[tool.pytest.ini_options]
|
|
41
|
+
pythonpath = ["src"]
|
|
@@ -272,57 +272,3 @@ def warning(txt, newline=True, stream=sys.stderr, prefix=False):
|
|
|
272
272
|
write('[RED:WARNING]: ', newline=False, stream=stream, )
|
|
273
273
|
|
|
274
274
|
write(txt, newline, stream)
|
|
275
|
-
|
|
276
|
-
################################################################################
|
|
277
|
-
|
|
278
|
-
if __name__ == '__main__':
|
|
279
|
-
for combo in (0, 1, 2):
|
|
280
|
-
print()
|
|
281
|
-
if combo == 0:
|
|
282
|
-
print('Background colours')
|
|
283
|
-
elif combo == 1:
|
|
284
|
-
print('Foreground colours')
|
|
285
|
-
else:
|
|
286
|
-
print('Combinations')
|
|
287
|
-
|
|
288
|
-
print()
|
|
289
|
-
for y in range(0, 16):
|
|
290
|
-
for x in range(0, 16):
|
|
291
|
-
colour = x + y * 16
|
|
292
|
-
|
|
293
|
-
if combo == 0:
|
|
294
|
-
write(format('[B%d]%4d' % (colour, colour)), newline=False)
|
|
295
|
-
elif combo == 1:
|
|
296
|
-
write(format('[%d]%4d' % (colour, colour)), newline=False)
|
|
297
|
-
else:
|
|
298
|
-
write(format('[B%d]%4d[%d]/%4d ' % (colour, colour, 255 - colour, 255 - colour)), newline=False)
|
|
299
|
-
|
|
300
|
-
write('[NORMAL]')
|
|
301
|
-
|
|
302
|
-
print()
|
|
303
|
-
|
|
304
|
-
write('Foreground: [RED]red [GREEN]green [BLACK]black [NORMAL]normal')
|
|
305
|
-
write('Background: [BRED]red [BGREEN]green [BBLACK]black [NORMAL]normal')
|
|
306
|
-
|
|
307
|
-
write('Foreground: [BBLUE:blue] [RED:red] normal')
|
|
308
|
-
|
|
309
|
-
write('Bright foreground: [RED_B]red [GREEN_B]green [BLACK_B]black [NORMAL]normal')
|
|
310
|
-
write('Bright background: [BRED_B]red [BGREEN_B]green [BBLACK_B]black [NORMAL]normal')
|
|
311
|
-
|
|
312
|
-
write('Foreground: [BBLUE:blue_B] [RED:red_B] normal')
|
|
313
|
-
|
|
314
|
-
print()
|
|
315
|
-
|
|
316
|
-
write('[NORMAL:Normal text]')
|
|
317
|
-
write('[FAINT:Faint text]')
|
|
318
|
-
write('[ITALIC:Italic text]')
|
|
319
|
-
write('[UNDERSCORE:Underscored text]')
|
|
320
|
-
write('[BLINK:Blinking text]')
|
|
321
|
-
write('[REVERSE:Reverse text]')
|
|
322
|
-
write('[STRIKE:Strikethrough text]')
|
|
323
|
-
|
|
324
|
-
print()
|
|
325
|
-
|
|
326
|
-
error('Error message (nothing should be output after this)', status=0)
|
|
327
|
-
|
|
328
|
-
write('This message should not appear')
|
|
@@ -79,19 +79,21 @@ class CursesDircolors:
|
|
|
79
79
|
""" Given a set of attributes return the equivalent curses colour pair,
|
|
80
80
|
creating a new one if a matching one doesn't already exsit """
|
|
81
81
|
|
|
82
|
-
# TODO: Take account of attributes as well as colours
|
|
83
|
-
|
|
84
82
|
colours = [attr['fore'], attr['back']]
|
|
85
83
|
|
|
86
84
|
# Get an existing colour pair that uses the same colours or create
|
|
87
|
-
# a new one if one doesn't exist
|
|
85
|
+
# a new one if one doesn't exist. If a pair slot is already allocated
|
|
86
|
+
# but the curses subsystem cannot allocate (e.g., no colours), skip init.
|
|
88
87
|
|
|
89
88
|
if colours in self.colour_pairs:
|
|
90
89
|
pair_index = self.colour_pairs.index(colours) + self.reserved
|
|
91
90
|
else:
|
|
92
91
|
pair_index = len(self.colour_pairs) + self.reserved
|
|
93
92
|
self.colour_pairs.append(colours)
|
|
94
|
-
|
|
93
|
+
try:
|
|
94
|
+
curses.init_pair(pair_index, attr['fore'], attr['back'])
|
|
95
|
+
except curses.error:
|
|
96
|
+
pass
|
|
95
97
|
|
|
96
98
|
return pair_index
|
|
97
99
|
|
|
@@ -131,8 +131,10 @@ class Dircolors:
|
|
|
131
131
|
Returns a boolean indicating whether any data was loaded.
|
|
132
132
|
The current database will always be cleared. """
|
|
133
133
|
self.clear()
|
|
134
|
+
need_close = False
|
|
134
135
|
if isinstance(database, str):
|
|
135
136
|
file = open(database, 'r')
|
|
137
|
+
need_close = True
|
|
136
138
|
elif isinstance(database, TextIOBase):
|
|
137
139
|
file = database
|
|
138
140
|
else:
|
|
@@ -157,6 +159,8 @@ class Dircolors:
|
|
|
157
159
|
continue # ignore TERM directives
|
|
158
160
|
elif key in _CODE_MAP:
|
|
159
161
|
self._codes[_CODE_MAP[key]] = val
|
|
162
|
+
elif key.startswith('*.'):
|
|
163
|
+
self._extensions[key[1:]] = val
|
|
160
164
|
elif key.startswith('.'):
|
|
161
165
|
self._extensions[key] = val
|
|
162
166
|
elif strict:
|
|
@@ -167,7 +171,8 @@ class Dircolors:
|
|
|
167
171
|
self._loaded = True
|
|
168
172
|
return self._loaded
|
|
169
173
|
finally:
|
|
170
|
-
|
|
174
|
+
if need_close:
|
|
175
|
+
file.close()
|
|
171
176
|
|
|
172
177
|
def load_defaults(self):
|
|
173
178
|
""" Load the default database. """
|
|
@@ -35,8 +35,9 @@ def instances(all=False):
|
|
|
35
35
|
try:
|
|
36
36
|
process = subprocess.run(cmd, capture_output=True, check=True, text=True)
|
|
37
37
|
|
|
38
|
-
for result in process.stdout:
|
|
39
|
-
|
|
38
|
+
for result in process.stdout.splitlines():
|
|
39
|
+
if result:
|
|
40
|
+
instances_list.append(result)
|
|
40
41
|
|
|
41
42
|
except subprocess.CalledProcessError as exc:
|
|
42
43
|
raise DockerError(exc)
|
|
@@ -48,12 +49,17 @@ def instances(all=False):
|
|
|
48
49
|
def stop(instance, force=False):
|
|
49
50
|
""" Stop the specified Docker instance """
|
|
50
51
|
|
|
51
|
-
|
|
52
|
+
cmd = ['docker', 'stop']
|
|
53
|
+
|
|
54
|
+
if force:
|
|
55
|
+
cmd.append('--force')
|
|
56
|
+
|
|
57
|
+
cmd.append(instance)
|
|
52
58
|
|
|
53
59
|
try:
|
|
54
|
-
subprocess.run(
|
|
60
|
+
subprocess.run(cmd, check=True, capture_output=False)
|
|
55
61
|
|
|
56
|
-
except
|
|
62
|
+
except subprocess.CalledProcessError as exc:
|
|
57
63
|
raise DockerError(exc)
|
|
58
64
|
|
|
59
65
|
################################################################################
|
|
@@ -80,10 +86,11 @@ def images():
|
|
|
80
86
|
""" Return a list of all current Docker images """
|
|
81
87
|
|
|
82
88
|
try:
|
|
83
|
-
process = subprocess.run(['docker', 'images', '-q'], capture_output=True, check=True)
|
|
89
|
+
process = subprocess.run(['docker', 'images', '-q'], capture_output=True, check=True, text=True)
|
|
84
90
|
|
|
85
|
-
for result in process:
|
|
86
|
-
|
|
91
|
+
for result in process.stdout.splitlines():
|
|
92
|
+
if result:
|
|
93
|
+
yield result
|
|
87
94
|
|
|
88
95
|
except subprocess.CalledProcessError as exc:
|
|
89
96
|
raise DockerError(exc)
|
|
@@ -19,7 +19,9 @@ def is_binary_file(filename):
|
|
|
19
19
|
""" Return True if there is a strong likelihood that the specified file
|
|
20
20
|
is binary. """
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
filetype = file_type(filename, mime=True)
|
|
23
|
+
|
|
24
|
+
return filetype.endswith('binary') if filetype else False
|
|
23
25
|
|
|
24
26
|
################################################################################
|
|
25
27
|
|
|
@@ -49,7 +51,7 @@ def format_size(size, always_suffix=False):
|
|
|
49
51
|
""" Convert a memory/disk size into appropriately-scaled units in bytes,
|
|
50
52
|
MiB, GiB, TiB as a string """
|
|
51
53
|
|
|
52
|
-
|
|
54
|
+
# Keep all the maths positive
|
|
53
55
|
|
|
54
56
|
if size < 0:
|
|
55
57
|
size = -size
|
|
@@ -933,7 +933,7 @@ def config_rm(section, key, source=LOCAL, path=None):
|
|
|
933
933
|
|
|
934
934
|
################################################################################
|
|
935
935
|
|
|
936
|
-
def ref(fields=('objectname'), sort=None, remotes=False, path=None):
|
|
936
|
+
def ref(fields=('objectname',), sort=None, remotes=False, path=None):
|
|
937
937
|
""" Wrapper for git for-each-ref """
|
|
938
938
|
|
|
939
939
|
cmd = ['for-each-ref']
|
|
@@ -958,7 +958,7 @@ def ref(fields=('objectname'), sort=None, remotes=False, path=None):
|
|
|
958
958
|
def branches(all=False, path=None, remote=False):
|
|
959
959
|
""" Return a list of all the branches in the current repo """
|
|
960
960
|
|
|
961
|
-
cmd = ['branch', '--format=%(refname:short)','--list']
|
|
961
|
+
cmd = ['branch', '--format=%(refname:short)', '--list']
|
|
962
962
|
|
|
963
963
|
if all:
|
|
964
964
|
cmd.append('--all')
|
|
@@ -32,7 +32,10 @@ def is_subdirectory(root_path, sub_path):
|
|
|
32
32
|
logging.debug('root path: %s', abs_root_path)
|
|
33
33
|
logging.debug('sub path : %s', abs_sub_path)
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
common = os.path.commonpath([abs_root_path, abs_sub_path])
|
|
36
|
+
|
|
37
|
+
# Require a strict subdirectory: common path matches root and paths differ
|
|
38
|
+
return common == abs_root_path and abs_sub_path != abs_root_path
|
|
36
39
|
|
|
37
40
|
################################################################################
|
|
38
41
|
|
|
@@ -63,6 +63,11 @@ class PopUp():
|
|
|
63
63
|
|
|
64
64
|
self.start_time = time.monotonic()
|
|
65
65
|
|
|
66
|
+
return self
|
|
67
|
+
|
|
68
|
+
def __exit__(self, _exc_type, _exc_value, _exc_traceback):
|
|
69
|
+
""" Remove the popup """
|
|
70
|
+
|
|
66
71
|
if self.waitkey:
|
|
67
72
|
while True:
|
|
68
73
|
keypress = self.screen.getch()
|
|
@@ -72,9 +77,6 @@ class PopUp():
|
|
|
72
77
|
curses.panel.update_panels()
|
|
73
78
|
self.screen.refresh()
|
|
74
79
|
|
|
75
|
-
def __exit__(self, _exc_type, _exc_value, _exc_traceback):
|
|
76
|
-
""" Remove the popup """
|
|
77
|
-
|
|
78
80
|
if self.panel:
|
|
79
81
|
if self.sleep:
|
|
80
82
|
elapsed = time.monotonic() - self.start_time
|
|
@@ -231,18 +231,19 @@ def _process(command,
|
|
|
231
231
|
|
|
232
232
|
# Wait until the command terminates (and set the returncode)
|
|
233
233
|
|
|
234
|
+
cmd.wait()
|
|
235
|
+
|
|
234
236
|
if stdout_thread:
|
|
235
237
|
stdout_thread.join()
|
|
236
238
|
|
|
237
239
|
if stderr_thread:
|
|
238
240
|
stderr_thread.join()
|
|
239
241
|
|
|
240
|
-
cmd.wait()
|
|
241
|
-
|
|
242
242
|
# If the command failed, raise an exception
|
|
243
243
|
|
|
244
244
|
if cmd.returncode:
|
|
245
|
-
raise RunError('\n'.join(stderr_data) if stderr_data else 'Error %d running "%s"' % (cmd.returncode, ' '.join(command))
|
|
245
|
+
raise RunError('\n'.join(stderr_data) if stderr_data else 'Error %d running "%s"' % (cmd.returncode, ' '.join(command)),
|
|
246
|
+
status=cmd.returncode)
|
|
246
247
|
|
|
247
248
|
# Return status, stdout, stderr (the latter 2 may be empty if we did not capture data).
|
|
248
249
|
|
|
@@ -57,7 +57,7 @@ RE_TIME = [
|
|
|
57
57
|
{'regex': re.compile(r'[0-9]{4}-[0-9]{2}-[0-9]{2}'), 'replace': '{DATE}'},
|
|
58
58
|
{'regex': re.compile(r'[0-9]{2}-[0-9]{2}-[0-9]{4}'), 'replace': '{DATE}'},
|
|
59
59
|
|
|
60
|
-
{'regex': re.compile(r'[0-9]([.][0-9]*)*\s*(second[s]?)'), 'replace': '{ELAPSED}'},
|
|
60
|
+
{'regex': re.compile(r'[0-9]+([.][0-9]*)*\s*(second[s]?)'), 'replace': '{ELAPSED}'},
|
|
61
61
|
|
|
62
62
|
{'find': '{DATE} {TIME}', 'replace': '{DATE+TIME}'},
|
|
63
63
|
{'regex': re.compile(r'[0-9]+m *[0-9]+s'), 'replace': '{ELAPSED}'},
|
|
@@ -83,7 +83,7 @@ RE_AWS = \
|
|
|
83
83
|
{'regex': re.compile(r'vol-0[0-9a-f]{16}'), 'replace': '{AMI-VOL}'},
|
|
84
84
|
{'regex': re.compile(r'sir-[0-9a-z]{8}'), 'replace': '{SPOT-INSTANCE}'},
|
|
85
85
|
{'regex': re.compile(r'i-0[0-9a-f]{16}'), 'replace': '{EC2-ID}'},
|
|
86
|
-
{'regex': re.compile(r'request id: [0-
|
|
86
|
+
{'regex': re.compile(r'request id: [0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'),
|
|
87
87
|
'replace': 'request id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'},
|
|
88
88
|
]
|
|
89
89
|
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: skilleter_modules
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.6
|
|
4
4
|
Summary: Modules used by my various Python projects (and hopefully useful to other people)
|
|
5
5
|
Author-email: John Skilleter <john@skilleter.org.uk>
|
|
6
6
|
Project-URL: Home, https://skilleter.org.uk
|
|
7
|
+
Project-URL: Repository, https://gitlab.com/skilleter/skilleter-modules
|
|
8
|
+
Project-URL: Issues, https://gitlab.com/skilleter/skilleter-extras/-/modules
|
|
7
9
|
Classifier: Programming Language :: Python :: 3
|
|
8
10
|
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
9
11
|
Classifier: Operating System :: OS Independent
|
{skilleter_modules-0.0.3 → skilleter_modules-0.0.6}/src/skilleter_modules.egg-info/SOURCES.txt
RENAMED
|
@@ -14,11 +14,21 @@ src/skilleter_modules/gitlab.py
|
|
|
14
14
|
src/skilleter_modules/path.py
|
|
15
15
|
src/skilleter_modules/popup.py
|
|
16
16
|
src/skilleter_modules/run.py
|
|
17
|
-
src/skilleter_modules/tfm_pane.py
|
|
18
17
|
src/skilleter_modules/tidy.py
|
|
19
18
|
src/skilleter_modules/venv_template.py
|
|
20
19
|
src/skilleter_modules.egg-info/PKG-INFO
|
|
21
20
|
src/skilleter_modules.egg-info/SOURCES.txt
|
|
22
21
|
src/skilleter_modules.egg-info/dependency_links.txt
|
|
23
22
|
src/skilleter_modules.egg-info/requires.txt
|
|
24
|
-
src/skilleter_modules.egg-info/top_level.txt
|
|
23
|
+
src/skilleter_modules.egg-info/top_level.txt
|
|
24
|
+
tests/test_colour.py
|
|
25
|
+
tests/test_dircolors.py
|
|
26
|
+
tests/test_docker.py
|
|
27
|
+
tests/test_files.py
|
|
28
|
+
tests/test_git.py
|
|
29
|
+
tests/test_gitlab.py
|
|
30
|
+
tests/test_path.py
|
|
31
|
+
tests/test_popup.py
|
|
32
|
+
tests/test_run.py
|
|
33
|
+
tests/test_tfm_pane.py
|
|
34
|
+
tests/test_tidy.py
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Minimal test code for colour"""
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
|
|
5
|
+
import skilleter_modules.colour as colour
|
|
6
|
+
|
|
7
|
+
def test_colour():
|
|
8
|
+
"""Very basic test"""
|
|
9
|
+
|
|
10
|
+
for combo in (0, 1, 2):
|
|
11
|
+
print()
|
|
12
|
+
if combo == 0:
|
|
13
|
+
print('Background colours')
|
|
14
|
+
elif combo == 1:
|
|
15
|
+
print('Foreground colours')
|
|
16
|
+
else:
|
|
17
|
+
print('Combinations')
|
|
18
|
+
|
|
19
|
+
print()
|
|
20
|
+
for y in range(0, 16):
|
|
21
|
+
for x in range(0, 16):
|
|
22
|
+
colour_index = x + y * 16
|
|
23
|
+
|
|
24
|
+
if combo == 0:
|
|
25
|
+
colour.write(format('[B%d]%4d' % (colour_index, colour_index)), newline=False)
|
|
26
|
+
elif combo == 1:
|
|
27
|
+
colour.write(format('[%d]%4d' % (colour_index, colour_index)), newline=False)
|
|
28
|
+
else:
|
|
29
|
+
colour.write(format('[B%d]%4d[%d]/%4d ' % (colour_index, colour_index, 255 - colour_index, 255 - colour_index)), newline=False)
|
|
30
|
+
|
|
31
|
+
colour.write('[NORMAL]')
|
|
32
|
+
|
|
33
|
+
print()
|
|
34
|
+
|
|
35
|
+
colour.write('Foreground: [RED]red [GREEN]green [BLACK]black [NORMAL]normal')
|
|
36
|
+
colour.write('Background: [BRED]red [BGREEN]green [BBLACK]black [NORMAL]normal')
|
|
37
|
+
|
|
38
|
+
colour.write('Foreground: [BBLUE:blue] [RED:red] normal')
|
|
39
|
+
|
|
40
|
+
colour.write('Bright foreground: [RED_B]red [GREEN_B]green [BLACK_B]black [NORMAL]normal')
|
|
41
|
+
colour.write('Bright background: [BRED_B]red [BGREEN_B]green [BBLACK_B]black [NORMAL]normal')
|
|
42
|
+
|
|
43
|
+
colour.write('Foreground: [BBLUE:blue_B] [RED:red_B] normal')
|
|
44
|
+
|
|
45
|
+
print()
|
|
46
|
+
|
|
47
|
+
colour.write('[NORMAL:Normal text]')
|
|
48
|
+
colour.write('[FAINT:Faint text]')
|
|
49
|
+
colour.write('[ITALIC:Italic text]')
|
|
50
|
+
colour.write('[UNDERSCORE:Underscored text]')
|
|
51
|
+
colour.write('[BLINK:Blinking text]')
|
|
52
|
+
colour.write('[REVERSE:Reverse text]')
|
|
53
|
+
colour.write('[STRIKE:Strikethrough text]')
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_write_empty_string_no_indent():
|
|
57
|
+
buf = io.StringIO()
|
|
58
|
+
colour.write('', indent=4, stream=buf)
|
|
59
|
+
assert buf.getvalue() == '\n'
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_write_with_indent_and_strip():
|
|
63
|
+
buf = io.StringIO()
|
|
64
|
+
colour.write(' text', indent=2, strip=True, stream=buf)
|
|
65
|
+
assert buf.getvalue() == ' text\n'
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import stat
|
|
3
|
+
|
|
4
|
+
import skilleter_modules.dircolors as dircolors
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_load_from_dircolors_does_not_close_stream():
|
|
8
|
+
data = io.StringIO('DIR 01;34\n')
|
|
9
|
+
dc = dircolors.Dircolors(load=False)
|
|
10
|
+
assert dc.load_from_dircolors(data, strict=True) is True
|
|
11
|
+
assert data.closed is False
|
|
12
|
+
assert dc.loaded is True
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_format_mode_directory_and_extension(tmp_path):
|
|
16
|
+
dc = dircolors.Dircolors(load=False)
|
|
17
|
+
# Load minimal rules: directories blue (01;34), *.txt red (31)
|
|
18
|
+
rules = 'DIR 01;34\n*.txt 31\n'
|
|
19
|
+
assert dc.load_from_dircolors(io.StringIO(rules), strict=True)
|
|
20
|
+
|
|
21
|
+
# Directory formatting: should wrap with escape codes
|
|
22
|
+
formatted_dir = dc.format_mode('name', (stat.S_IFDIR | stat.S_IRUSR))
|
|
23
|
+
assert '\x1b[' in formatted_dir and formatted_dir.endswith('\x1b[0m')
|
|
24
|
+
|
|
25
|
+
# Extension formatting: .txt uses red foreground
|
|
26
|
+
formatted_file = dc.format_mode('file.txt', 0)
|
|
27
|
+
assert '\x1b[' in formatted_file and formatted_file.endswith('\x1b[0m')
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import types
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
import skilleter_modules.docker as docker
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DummyResult(types.SimpleNamespace):
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_instances_parses_lines(monkeypatch):
|
|
13
|
+
def fake_run(cmd, capture_output, check, text):
|
|
14
|
+
assert cmd[:2] == ['docker', 'ps']
|
|
15
|
+
return DummyResult(stdout='id1\nid2\n')
|
|
16
|
+
|
|
17
|
+
monkeypatch.setattr('subprocess.run', fake_run)
|
|
18
|
+
assert docker.instances() == ['id1', 'id2']
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_stop_force_passes_flag(monkeypatch):
|
|
22
|
+
seen = {}
|
|
23
|
+
|
|
24
|
+
def fake_run(cmd, check, capture_output):
|
|
25
|
+
seen['cmd'] = cmd
|
|
26
|
+
return DummyResult()
|
|
27
|
+
|
|
28
|
+
monkeypatch.setattr('subprocess.run', fake_run)
|
|
29
|
+
docker.stop('abc', force=True)
|
|
30
|
+
assert seen['cmd'] == ['docker', 'stop', '--force', 'abc']
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
import skilleter_modules.files as files
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_format_size_suffix_and_sign():
|
|
7
|
+
assert files.format_size(-1024, always_suffix=True).startswith('-')
|
|
8
|
+
assert files.format_size(1024, always_suffix=True).endswith(' KiB')
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_backup_no_file_no_raise(tmp_path):
|
|
12
|
+
missing = tmp_path / 'missing.txt'
|
|
13
|
+
# Should no-op when file is absent
|
|
14
|
+
files.backup(str(missing))
|
|
15
|
+
assert not list(tmp_path.iterdir())
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_backup_creates_copy(tmp_path):
|
|
19
|
+
src = tmp_path / 'file.txt'
|
|
20
|
+
src.write_text('data', encoding='utf-8')
|
|
21
|
+
files.backup(str(src))
|
|
22
|
+
assert (tmp_path / 'file.bak').read_text(encoding='utf-8') == 'data'
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import skilleter_modules.path as path
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_is_subdirectory_strict_descendant():
|
|
5
|
+
assert path.is_subdirectory('/root', '/root/child') is True
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_is_subdirectory_same_path_is_false():
|
|
9
|
+
assert path.is_subdirectory('/root', '/root') is False
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_is_subdirectory_unrelated_is_false():
|
|
13
|
+
assert path.is_subdirectory('/root/one', '/root/two') is False
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_trimpath_home_to_tilde(monkeypatch):
|
|
17
|
+
monkeypatch.setenv('HOME', '/home/user')
|
|
18
|
+
assert path.trimpath('/home/user/projects', 80).startswith('~/')
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_trimpath_truncates_middle():
|
|
22
|
+
trimmed = path.trimpath('/a/b/c/d/e/f/g/h/i', 10)
|
|
23
|
+
assert '...' in trimmed
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from types import SimpleNamespace
|
|
2
|
+
|
|
3
|
+
import skilleter_modules.popup as popup
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FakePanel:
|
|
7
|
+
def __init__(self):
|
|
8
|
+
self.update_calls = 0
|
|
9
|
+
self.new_calls = []
|
|
10
|
+
|
|
11
|
+
def new_panel(self, window):
|
|
12
|
+
self.new_calls.append(window)
|
|
13
|
+
return SimpleNamespace(top=lambda: None)
|
|
14
|
+
|
|
15
|
+
def update_panels(self):
|
|
16
|
+
self.update_calls += 1
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class FakeWindow:
|
|
20
|
+
def __init__(self):
|
|
21
|
+
self.add_calls = []
|
|
22
|
+
self.bkgd_calls = []
|
|
23
|
+
|
|
24
|
+
def bkgd(self, ch, colour):
|
|
25
|
+
self.bkgd_calls.append((ch, colour))
|
|
26
|
+
|
|
27
|
+
def addstr(self, y_pos, x_pos, line, colour):
|
|
28
|
+
self.add_calls.append((y_pos, x_pos, line, colour))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class FakeScreen:
|
|
32
|
+
def __init__(self, keys):
|
|
33
|
+
self.keys = list(keys)
|
|
34
|
+
self.getch_calls = 0
|
|
35
|
+
self.refresh_calls = 0
|
|
36
|
+
|
|
37
|
+
def getmaxyx(self):
|
|
38
|
+
return (24, 80)
|
|
39
|
+
|
|
40
|
+
def refresh(self):
|
|
41
|
+
self.refresh_calls += 1
|
|
42
|
+
|
|
43
|
+
def getch(self):
|
|
44
|
+
self.getch_calls += 1
|
|
45
|
+
return self.keys.pop(0)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_waitkey_occurs_on_exit(monkeypatch):
|
|
49
|
+
fake_panel = FakePanel()
|
|
50
|
+
|
|
51
|
+
fake_curses = SimpleNamespace(
|
|
52
|
+
KEY_RESIZE=999,
|
|
53
|
+
panel=fake_panel,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
fake_curses.color_pair = lambda colour: f'colour-{colour}'
|
|
57
|
+
fake_curses.newwin = lambda h, w, y, x: FakeWindow()
|
|
58
|
+
|
|
59
|
+
monkeypatch.setattr(popup, 'curses', fake_curses)
|
|
60
|
+
|
|
61
|
+
keys = [fake_curses.KEY_RESIZE, ord('a')]
|
|
62
|
+
screen = FakeScreen(keys)
|
|
63
|
+
|
|
64
|
+
with popup.PopUp(screen, 'hello', 3, waitkey=True):
|
|
65
|
+
enter_updates = fake_panel.update_calls
|
|
66
|
+
assert screen.getch_calls == 0
|
|
67
|
+
|
|
68
|
+
assert fake_panel.update_calls == enter_updates + 1
|
|
69
|
+
assert screen.getch_calls == 2
|
|
70
|
+
assert screen.refresh_calls >= 1
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
|
|
3
|
+
import skilleter_modules.run as run
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_process_echoes_and_captures_stdout(monkeypatch):
|
|
7
|
+
real_popen = subprocess.Popen
|
|
8
|
+
|
|
9
|
+
def fake_popen(command, bufsize=0, stdout=None, stderr=None, text=False, errors=None, encoding=None, **kwargs):
|
|
10
|
+
assert stdout == subprocess.PIPE
|
|
11
|
+
assert stderr == subprocess.PIPE
|
|
12
|
+
proc = real_popen(['printf', 'hello\n'], stdout=stdout, stderr=stderr, text=True)
|
|
13
|
+
return proc
|
|
14
|
+
|
|
15
|
+
monkeypatch.setattr(subprocess, 'Popen', fake_popen)
|
|
16
|
+
result = run._process(['printf', 'hello'])
|
|
17
|
+
assert result['stdout'] == ['hello']
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_process_raises_on_nonzero(monkeypatch):
|
|
21
|
+
real_popen = subprocess.Popen
|
|
22
|
+
|
|
23
|
+
def fake_popen(command, bufsize=0, stdout=None, stderr=None, text=False, errors=None, encoding=None, **kwargs):
|
|
24
|
+
return real_popen(['python', '-c', 'import sys; sys.exit(3)'], stdout=stdout, stderr=stderr, text=True)
|
|
25
|
+
|
|
26
|
+
monkeypatch.setattr(subprocess, 'Popen', fake_popen)
|
|
27
|
+
try:
|
|
28
|
+
run._process(['python', '-c', 'exit 3'])
|
|
29
|
+
except run.RunError as exc:
|
|
30
|
+
assert exc.status == 3
|
|
31
|
+
else:
|
|
32
|
+
assert False, 'expected RunError'
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import skilleter_modules.tidy as tidy
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_remove_times_does_not_match_stray_dot():
|
|
5
|
+
text = 'Version 1. release'
|
|
6
|
+
assert tidy.remove_times(text) == text
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_remove_times_replaces_seconds():
|
|
10
|
+
assert tidy.remove_times('took 12.5 seconds') == 'took {ELAPSED}'
|
|
@@ -1,593 +0,0 @@
|
|
|
1
|
-
################################################################################
|
|
2
|
-
""" Pane class for tfm """
|
|
3
|
-
################################################################################
|
|
4
|
-
|
|
5
|
-
import sys
|
|
6
|
-
import os
|
|
7
|
-
import curses
|
|
8
|
-
import fnmatch
|
|
9
|
-
import stat
|
|
10
|
-
import glob
|
|
11
|
-
import time
|
|
12
|
-
import threading
|
|
13
|
-
|
|
14
|
-
from enum import IntEnum
|
|
15
|
-
|
|
16
|
-
if sys.platform == 'linux':
|
|
17
|
-
import inotify.adapters
|
|
18
|
-
|
|
19
|
-
from . import dc_curses
|
|
20
|
-
from . import path
|
|
21
|
-
from . import popup
|
|
22
|
-
|
|
23
|
-
################################################################################
|
|
24
|
-
|
|
25
|
-
class SortOrder(IntEnum):
|
|
26
|
-
""" Sort order for filename list """
|
|
27
|
-
|
|
28
|
-
FILENAME = 0
|
|
29
|
-
EXTENSION = 1
|
|
30
|
-
MODIFIED_DATE = 2
|
|
31
|
-
SIZE = 3
|
|
32
|
-
NUM_SORTS = 4
|
|
33
|
-
|
|
34
|
-
SORT_TYPE = ('filename', 'extension', 'modified date', 'size')
|
|
35
|
-
|
|
36
|
-
################################################################################
|
|
37
|
-
|
|
38
|
-
def inotify_wait(self):
|
|
39
|
-
"""Thread to wait for inotify events and post an event to the queue if there
|
|
40
|
-
any create/delete/modify events in the current directory.
|
|
41
|
-
Sends no more than 1 update per second to avoid drowning the recipient."""
|
|
42
|
-
|
|
43
|
-
while True:
|
|
44
|
-
trigger = False
|
|
45
|
-
for event in self.ino.event_gen(yield_nones=False, timeout_s=1):
|
|
46
|
-
(_, events, path, _) = event
|
|
47
|
-
|
|
48
|
-
if path == self.current_dir and ('IN_CREATE' in events or 'IN_DELETE' in events or 'IN_MODIFY' in events):
|
|
49
|
-
trigger = True
|
|
50
|
-
|
|
51
|
-
if trigger:
|
|
52
|
-
self.event_queue.put(('inotify', self.index))
|
|
53
|
-
|
|
54
|
-
################################################################################
|
|
55
|
-
|
|
56
|
-
class Pane():
|
|
57
|
-
""" Class for a file manager pane """
|
|
58
|
-
|
|
59
|
-
def __init__(self, index, num_panes, colours, event_queue):
|
|
60
|
-
# Create window for the pane (dummy size and position initially)
|
|
61
|
-
|
|
62
|
-
self.screen = curses.newwin(1, 1, 0, 0)
|
|
63
|
-
|
|
64
|
-
self.index = index
|
|
65
|
-
|
|
66
|
-
self.current_dir = ''
|
|
67
|
-
|
|
68
|
-
self.ino = inotify.adapters.Inotify() if sys.platform == 'linux' else None
|
|
69
|
-
|
|
70
|
-
self.set_current_dir(os.getcwd())
|
|
71
|
-
|
|
72
|
-
self.event_queue = event_queue
|
|
73
|
-
|
|
74
|
-
if sys.platform == 'linux':
|
|
75
|
-
inotify_thread = threading.Thread(target=inotify_wait, args=(self,), daemon=True)
|
|
76
|
-
inotify_thread.start()
|
|
77
|
-
|
|
78
|
-
# Default sort order
|
|
79
|
-
|
|
80
|
-
self.sort_order = SortOrder.FILENAME
|
|
81
|
-
self.reverse_sort = False
|
|
82
|
-
|
|
83
|
-
# Set the attributes of the current review (some are initialised
|
|
84
|
-
# when the screen is drawn)
|
|
85
|
-
|
|
86
|
-
# Index of the current file in the filtered_file_indices
|
|
87
|
-
|
|
88
|
-
self.current = 0
|
|
89
|
-
|
|
90
|
-
self.offset = 0
|
|
91
|
-
self.num_panes = num_panes
|
|
92
|
-
self.colours = colours
|
|
93
|
-
|
|
94
|
-
self.searchstring = None
|
|
95
|
-
|
|
96
|
-
self.height = self.width = -1
|
|
97
|
-
self.file_list_y = 1
|
|
98
|
-
self.file_list_h = -1
|
|
99
|
-
|
|
100
|
-
# File list is a list of the files in the current directory
|
|
101
|
-
|
|
102
|
-
self.file_list = []
|
|
103
|
-
|
|
104
|
-
# Filtered file list is a list of the indices in file_list of the visible files
|
|
105
|
-
# in the current directory
|
|
106
|
-
|
|
107
|
-
self.filtered_file_indices = []
|
|
108
|
-
|
|
109
|
-
# Set of the names of currently-tagged files
|
|
110
|
-
|
|
111
|
-
self.tagged_set = set()
|
|
112
|
-
|
|
113
|
-
self.in_filter = self.out_filter = None
|
|
114
|
-
self.hide_hidden_filter = True
|
|
115
|
-
|
|
116
|
-
self.file_display_fields = ['size', 'mtime']
|
|
117
|
-
|
|
118
|
-
# Set up dircolor highlighting
|
|
119
|
-
|
|
120
|
-
self.dircolours = dc_curses.CursesDircolors(reserved=self.colours['reserved_colours'])
|
|
121
|
-
|
|
122
|
-
# Generate the list of files to be shown (takes filtering into account)
|
|
123
|
-
|
|
124
|
-
self.update_files()
|
|
125
|
-
|
|
126
|
-
################################################################################
|
|
127
|
-
|
|
128
|
-
def sort_file_list(self):
|
|
129
|
-
""" Sort the file list according to the current sort order """
|
|
130
|
-
|
|
131
|
-
if self.sort_order == SortOrder.FILENAME:
|
|
132
|
-
self.file_list.sort(reverse=self.reverse_sort, key=lambda entry: (not entry['isdir'], os.path.basename(entry['name'])))
|
|
133
|
-
elif self.sort_order == SortOrder.EXTENSION:
|
|
134
|
-
self.file_list.sort(reverse=self.reverse_sort, key=lambda entry: (not entry['isdir'], entry['name'].split('.')[-1]))
|
|
135
|
-
elif self.sort_order == SortOrder.MODIFIED_DATE:
|
|
136
|
-
self.file_list.sort(reverse=self.reverse_sort, key=lambda entry: (not entry['isdir'], entry['mtime']))
|
|
137
|
-
elif self.sort_order == SortOrder.SIZE:
|
|
138
|
-
self.file_list.sort(reverse=self.reverse_sort, key=lambda entry: (not entry['isdir'], entry['size']))
|
|
139
|
-
|
|
140
|
-
################################################################################
|
|
141
|
-
|
|
142
|
-
def update_files(self):
|
|
143
|
-
""" Get the list of files
|
|
144
|
-
"""
|
|
145
|
-
|
|
146
|
-
def file_stats(filename):
|
|
147
|
-
""" Get the stats for a file """
|
|
148
|
-
|
|
149
|
-
filestat = os.stat(filename, follow_symlinks=False)
|
|
150
|
-
|
|
151
|
-
info = {'name': filename,
|
|
152
|
-
'mode': filestat.st_mode,
|
|
153
|
-
'uid': filestat.st_uid,
|
|
154
|
-
'gid': filestat.st_gid,
|
|
155
|
-
'size': filestat.st_size,
|
|
156
|
-
'atime': filestat.st_atime,
|
|
157
|
-
'mtime': filestat.st_mtime,
|
|
158
|
-
'ctime': filestat.st_ctime,
|
|
159
|
-
'isdir': stat.S_ISDIR(filestat.st_mode)}
|
|
160
|
-
|
|
161
|
-
return info
|
|
162
|
-
|
|
163
|
-
# Rebuild the file list
|
|
164
|
-
|
|
165
|
-
self.file_list = []
|
|
166
|
-
for filename in glob.glob(os.path.join(self.current_dir, '*')) + glob.glob(os.path.join(self.current_dir, '.*')):
|
|
167
|
-
self.file_list.append(file_stats(filename))
|
|
168
|
-
|
|
169
|
-
# Update the tagged file list to contain only current files
|
|
170
|
-
|
|
171
|
-
self.tagged_set = {entry['name'] for entry in self.file_list if entry['name'] in self.tagged_set}
|
|
172
|
-
|
|
173
|
-
# Optionally add '..' as an entry
|
|
174
|
-
|
|
175
|
-
if self.current_dir != '/':
|
|
176
|
-
self.file_list.append(file_stats('..'))
|
|
177
|
-
|
|
178
|
-
self.sort_file_list()
|
|
179
|
-
self.update_file_list()
|
|
180
|
-
|
|
181
|
-
################################################################################
|
|
182
|
-
|
|
183
|
-
def update_file_list(self):
|
|
184
|
-
""" Generate the file list from the list of current files with filtering
|
|
185
|
-
applied if enabled """
|
|
186
|
-
|
|
187
|
-
self.sort_file_list()
|
|
188
|
-
|
|
189
|
-
if self.active_filters():
|
|
190
|
-
self.filtered_file_indices = [i for i, entry in enumerate(self.file_list) if not self.filtered(entry)]
|
|
191
|
-
else:
|
|
192
|
-
self.filtered_file_indices = range(len(self.file_list))
|
|
193
|
-
|
|
194
|
-
################################################################################
|
|
195
|
-
|
|
196
|
-
def active_filters(self):
|
|
197
|
-
""" Return true if any filters are active """
|
|
198
|
-
|
|
199
|
-
return self.out_filter or \
|
|
200
|
-
self.in_filter or \
|
|
201
|
-
self.hide_hidden_filter
|
|
202
|
-
|
|
203
|
-
################################################################################
|
|
204
|
-
|
|
205
|
-
def filtered(self, entry):
|
|
206
|
-
""" Return True if an entry is hidden by one or more filters """
|
|
207
|
-
|
|
208
|
-
result = False
|
|
209
|
-
|
|
210
|
-
if self.out_filter and fnmatch.fnmatch(entry['name'], self.out_filter):
|
|
211
|
-
result = True
|
|
212
|
-
|
|
213
|
-
elif self.in_filter and not fnmatch.fnmatch(entry['name'], self.in_filter):
|
|
214
|
-
result = True
|
|
215
|
-
|
|
216
|
-
elif self.hide_hidden_filter:
|
|
217
|
-
base_name = os.path.basename(entry['name'])
|
|
218
|
-
if base_name[0] == '.' and base_name != '..':
|
|
219
|
-
result = True
|
|
220
|
-
|
|
221
|
-
return result
|
|
222
|
-
|
|
223
|
-
################################################################################
|
|
224
|
-
|
|
225
|
-
def constrain_display_parameters(self):
|
|
226
|
-
""" Ensure that the current display parameters are within range - easier
|
|
227
|
-
to do it in one place for all of them than check individually whenever we
|
|
228
|
-
change any of them """
|
|
229
|
-
|
|
230
|
-
self.current = max(min(self.current, len(self.filtered_file_indices) - 1), 0)
|
|
231
|
-
self.offset = min(len(self.filtered_file_indices) - 1, max(0, self.offset))
|
|
232
|
-
|
|
233
|
-
# Keep the current entry on-screen
|
|
234
|
-
|
|
235
|
-
if self.current >= self.offset + self.height - 2:
|
|
236
|
-
self.offset = self.current
|
|
237
|
-
elif self.current < self.offset:
|
|
238
|
-
self.offset = self.current
|
|
239
|
-
|
|
240
|
-
################################################################################
|
|
241
|
-
|
|
242
|
-
def file_info_display(self, filename):
|
|
243
|
-
""" Extract the additional file info fields displayed to the right
|
|
244
|
-
of the filename """
|
|
245
|
-
|
|
246
|
-
data = []
|
|
247
|
-
for field in self.file_display_fields:
|
|
248
|
-
if field == 'name':
|
|
249
|
-
data.append(filename['name'])
|
|
250
|
-
elif field in ('atime', 'mtime', 'ctime'):
|
|
251
|
-
data.append(time.strftime('%x %X', time.gmtime(filename[field])))
|
|
252
|
-
elif field == 'uid':
|
|
253
|
-
pass
|
|
254
|
-
elif field == 'gid':
|
|
255
|
-
pass
|
|
256
|
-
elif field == 'mode':
|
|
257
|
-
pass
|
|
258
|
-
elif field == 'size':
|
|
259
|
-
data.append(str(filename[field]))
|
|
260
|
-
|
|
261
|
-
return ' '.join(data)
|
|
262
|
-
|
|
263
|
-
################################################################################
|
|
264
|
-
|
|
265
|
-
def show_file_list(self, current_pane):
|
|
266
|
-
""" Draw the current page of the file list """
|
|
267
|
-
|
|
268
|
-
for ypos in range(0, self.file_list_h):
|
|
269
|
-
|
|
270
|
-
normal_colour = curses.color_pair(self.colours['normal'])
|
|
271
|
-
|
|
272
|
-
if 0 <= self.offset + ypos < len(self.filtered_file_indices):
|
|
273
|
-
# Work out what colour to render the file details in
|
|
274
|
-
|
|
275
|
-
current_file = self.file_list[self.filtered_file_indices[self.offset + ypos]]
|
|
276
|
-
|
|
277
|
-
current = self.offset + ypos == self.current
|
|
278
|
-
|
|
279
|
-
# The text to render
|
|
280
|
-
|
|
281
|
-
filename = os.path.basename(current_file['name'])
|
|
282
|
-
|
|
283
|
-
data = self.file_info_display(current_file)
|
|
284
|
-
|
|
285
|
-
name = f'/{filename}' if current_file['isdir'] else filename
|
|
286
|
-
name = f'* {name}' if current_file['name'] in self.tagged_set else f' {name}'
|
|
287
|
-
|
|
288
|
-
if len(name) > self.width - len(data):
|
|
289
|
-
entry = name[:self.width - 3] + '...'
|
|
290
|
-
else:
|
|
291
|
-
entry = name + ' ' * (self.width - len(name) - len(data)) + data
|
|
292
|
-
|
|
293
|
-
file_colour = self.dircolours.get_colour_pair(current_file['name'], current_file['mode'])
|
|
294
|
-
else:
|
|
295
|
-
filename = entry = None
|
|
296
|
-
current = False
|
|
297
|
-
file_colour = normal_colour
|
|
298
|
-
|
|
299
|
-
# Reverse the colours if this the cursor line
|
|
300
|
-
|
|
301
|
-
if current and current_pane:
|
|
302
|
-
file_colour |= curses.A_REVERSE
|
|
303
|
-
normal_colour |= curses.A_REVERSE
|
|
304
|
-
|
|
305
|
-
# Write the prefix, filename, and, if necessary, padding
|
|
306
|
-
|
|
307
|
-
self.screen.move(self.file_list_y + ypos, 0)
|
|
308
|
-
if entry:
|
|
309
|
-
self.screen.addstr(entry, file_colour)
|
|
310
|
-
else:
|
|
311
|
-
self.screen.clrtoeol()
|
|
312
|
-
|
|
313
|
-
# if len(filename) < self.width:
|
|
314
|
-
# self.screen.addstr(self.file_list_y + ypos, len(filename), ' ' * (self.width - len(filename)), normal_colour)
|
|
315
|
-
|
|
316
|
-
current_dir = path.trimpath(self.current_dir, self.width)
|
|
317
|
-
|
|
318
|
-
self.screen.move(0, 0)
|
|
319
|
-
self.screen.attron(curses.color_pair(self.colours['status']))
|
|
320
|
-
self.screen.addstr(current_dir + ' '*(self.width-len(current_dir)))
|
|
321
|
-
|
|
322
|
-
self.screen.refresh()
|
|
323
|
-
|
|
324
|
-
if not self.filtered_file_indices:
|
|
325
|
-
with popup.PopUp(self.screen, 'All files are hidden - Press \'c\' to clear filters.', self.colours['status']):
|
|
326
|
-
pass
|
|
327
|
-
|
|
328
|
-
################################################################################
|
|
329
|
-
|
|
330
|
-
def filter_description(self):
|
|
331
|
-
""" Return a textual description of the active filters """
|
|
332
|
-
|
|
333
|
-
filters = []
|
|
334
|
-
|
|
335
|
-
if self.out_filter:
|
|
336
|
-
filters.append('filter-out wildcard')
|
|
337
|
-
|
|
338
|
-
if self.in_filter:
|
|
339
|
-
filters.append('filter-in wildcard')
|
|
340
|
-
|
|
341
|
-
return ', '.join(filters)
|
|
342
|
-
|
|
343
|
-
################################################################################
|
|
344
|
-
|
|
345
|
-
def clear_filters(self):
|
|
346
|
-
""" Clear all filters """
|
|
347
|
-
|
|
348
|
-
if self.out_filter or self.in_filter:
|
|
349
|
-
self.out_filter = self.in_filter = None
|
|
350
|
-
self.update_file_list()
|
|
351
|
-
|
|
352
|
-
################################################################################
|
|
353
|
-
|
|
354
|
-
def reload_changes(self):
|
|
355
|
-
""" Update the list of files in case something external has
|
|
356
|
-
changed it. """
|
|
357
|
-
|
|
358
|
-
self.update_files()
|
|
359
|
-
|
|
360
|
-
################################################################################
|
|
361
|
-
|
|
362
|
-
def get_current_dir(self):
|
|
363
|
-
""" Get the current directory for the pane """
|
|
364
|
-
|
|
365
|
-
return self.current_dir
|
|
366
|
-
|
|
367
|
-
################################################################################
|
|
368
|
-
|
|
369
|
-
def set_current_dir(self, directory):
|
|
370
|
-
""" Set the current directory for the pane """
|
|
371
|
-
|
|
372
|
-
if self.current_dir and self.ino:
|
|
373
|
-
self.ino.remove_watch(self.current_dir)
|
|
374
|
-
|
|
375
|
-
self.current_dir = os.path.normpath(directory)
|
|
376
|
-
|
|
377
|
-
if self.ino:
|
|
378
|
-
self.ino.add_watch(directory)
|
|
379
|
-
|
|
380
|
-
################################################################################
|
|
381
|
-
|
|
382
|
-
def get_current_file(self):
|
|
383
|
-
""" Get the current file for the pane """
|
|
384
|
-
|
|
385
|
-
return self.file_list[self.filtered_file_indices[self.current]]
|
|
386
|
-
|
|
387
|
-
################################################################################
|
|
388
|
-
|
|
389
|
-
def get_tagged_files(self):
|
|
390
|
-
""" Get the list of tagged files, or the current file if none are tagged """
|
|
391
|
-
|
|
392
|
-
if self.tagged_set:
|
|
393
|
-
return [self.file_list[entry] for entry in self.filtered_file_indices if self.file_list[entry]['name'] in self.tagged_set]
|
|
394
|
-
|
|
395
|
-
return [self.get_current_file()]
|
|
396
|
-
|
|
397
|
-
################################################################################
|
|
398
|
-
|
|
399
|
-
def search_entry(self, searchstring):
|
|
400
|
-
""" Search for the next match with the specified search string """
|
|
401
|
-
|
|
402
|
-
for i in list(range(self.current + 1, len(self.filtered_file_indices))) + list(range(0, self.current)):
|
|
403
|
-
if fnmatch.fnmatch(os.path.basename(self.file_list[self.filtered_file_indices[i]]['name']), searchstring):
|
|
404
|
-
self.current = i
|
|
405
|
-
break
|
|
406
|
-
|
|
407
|
-
################################################################################
|
|
408
|
-
|
|
409
|
-
def search_match(self, searchstring):
|
|
410
|
-
""" Search for the first match """
|
|
411
|
-
|
|
412
|
-
self.searchstring = searchstring
|
|
413
|
-
self.search_next_match()
|
|
414
|
-
|
|
415
|
-
################################################################################
|
|
416
|
-
|
|
417
|
-
def search_next_match(self):
|
|
418
|
-
""" Search for the next match with the current search string """
|
|
419
|
-
|
|
420
|
-
self.search_entry(self.searchstring)
|
|
421
|
-
|
|
422
|
-
################################################################################
|
|
423
|
-
|
|
424
|
-
def move_end(self):
|
|
425
|
-
""" Move to the end of the file list """
|
|
426
|
-
|
|
427
|
-
self.current = len(self.filtered_file_indices) - 1
|
|
428
|
-
|
|
429
|
-
################################################################################
|
|
430
|
-
|
|
431
|
-
def move_top(self):
|
|
432
|
-
""" Move to the top of the file list """
|
|
433
|
-
|
|
434
|
-
self.current = self.offset = 0
|
|
435
|
-
|
|
436
|
-
################################################################################
|
|
437
|
-
|
|
438
|
-
def move_to_file(self, filename):
|
|
439
|
-
""" Move to the specified file (if it exists) in the current directory
|
|
440
|
-
or to the top if not """
|
|
441
|
-
|
|
442
|
-
self.current = self.offset = 0
|
|
443
|
-
if filename:
|
|
444
|
-
self.search_entry(filename)
|
|
445
|
-
|
|
446
|
-
################################################################################
|
|
447
|
-
|
|
448
|
-
def move(self, delta):
|
|
449
|
-
""" Move up or down the file list """
|
|
450
|
-
|
|
451
|
-
self.current += delta
|
|
452
|
-
|
|
453
|
-
################################################################################
|
|
454
|
-
|
|
455
|
-
def filter_out(self, filter_out):
|
|
456
|
-
""" Set an exclusion filter """
|
|
457
|
-
|
|
458
|
-
self.out_filter = filter_out
|
|
459
|
-
self.in_filter = None
|
|
460
|
-
self.update_file_list()
|
|
461
|
-
|
|
462
|
-
################################################################################
|
|
463
|
-
|
|
464
|
-
def filter_in(self, filter_in):
|
|
465
|
-
""" Set an inclusion filter """
|
|
466
|
-
|
|
467
|
-
self.in_filter = filter_in
|
|
468
|
-
self.out_filter = None
|
|
469
|
-
self.update_file_list()
|
|
470
|
-
|
|
471
|
-
################################################################################
|
|
472
|
-
|
|
473
|
-
def move_page_down(self):
|
|
474
|
-
""" Page down """
|
|
475
|
-
|
|
476
|
-
pos = self.current - self.offset
|
|
477
|
-
self.offset += self.file_list_h - 1
|
|
478
|
-
self.current = self.offset + pos
|
|
479
|
-
|
|
480
|
-
################################################################################
|
|
481
|
-
|
|
482
|
-
def move_page_up(self):
|
|
483
|
-
""" Page up """
|
|
484
|
-
|
|
485
|
-
pos = self.current - self.offset
|
|
486
|
-
self.offset -= self.file_list_h - 1
|
|
487
|
-
self.current = self.offset + pos
|
|
488
|
-
|
|
489
|
-
################################################################################
|
|
490
|
-
|
|
491
|
-
def set_sort_order(self, value):
|
|
492
|
-
""" Set the sort order """
|
|
493
|
-
|
|
494
|
-
self.sort_order = (self.sort_order + value) % SortOrder.NUM_SORTS
|
|
495
|
-
|
|
496
|
-
self.update_sort()
|
|
497
|
-
|
|
498
|
-
################################################################################
|
|
499
|
-
|
|
500
|
-
def get_sort_order(self):
|
|
501
|
-
""" Get the current sort order """
|
|
502
|
-
|
|
503
|
-
return self.sort_order
|
|
504
|
-
|
|
505
|
-
################################################################################
|
|
506
|
-
|
|
507
|
-
def sort_type_msg(self):
|
|
508
|
-
""" Return a textual explanation of the current sort type """
|
|
509
|
-
|
|
510
|
-
if self.reverse_sort:
|
|
511
|
-
msg = f'Reverse-sorting by {SORT_TYPE[self.sort_order]}'
|
|
512
|
-
else:
|
|
513
|
-
msg = f'Sorting by {SORT_TYPE[self.sort_order]}'
|
|
514
|
-
|
|
515
|
-
return msg
|
|
516
|
-
|
|
517
|
-
################################################################################
|
|
518
|
-
|
|
519
|
-
def reverse_sort_order(self):
|
|
520
|
-
""" Reverse the sort order """
|
|
521
|
-
|
|
522
|
-
self.reverse_sort = not self.reverse_sort
|
|
523
|
-
self.update_sort()
|
|
524
|
-
|
|
525
|
-
################################################################################
|
|
526
|
-
|
|
527
|
-
def update_sort(self):
|
|
528
|
-
""" Update the sort """
|
|
529
|
-
|
|
530
|
-
msg = self.sort_type_msg()
|
|
531
|
-
|
|
532
|
-
with popup.PopUp(self.screen, msg, self.colours['status']):
|
|
533
|
-
self.update_file_list()
|
|
534
|
-
|
|
535
|
-
################################################################################
|
|
536
|
-
|
|
537
|
-
def set_pane_coords(self, y, x, height, width):
|
|
538
|
-
""" Set the pane height given the pane display area """
|
|
539
|
-
|
|
540
|
-
pane_width = width//self.num_panes
|
|
541
|
-
|
|
542
|
-
self.height = height
|
|
543
|
-
self.file_list_h = height-1
|
|
544
|
-
self.width = pane_width-1 # TODO: Why '-1'?
|
|
545
|
-
self.screen.resize(height, pane_width)
|
|
546
|
-
self.screen.mvwin(y, x + pane_width*self.index)
|
|
547
|
-
|
|
548
|
-
################################################################################
|
|
549
|
-
|
|
550
|
-
def tag_current(self):
|
|
551
|
-
""" Tag the current entry (unless it is '..') """
|
|
552
|
-
|
|
553
|
-
current = self.file_list[self.filtered_file_indices[self.current]]['name']
|
|
554
|
-
|
|
555
|
-
if current != '..':
|
|
556
|
-
if current in self.tagged_set:
|
|
557
|
-
self.tagged_set.remove(current)
|
|
558
|
-
else:
|
|
559
|
-
self.tagged_set.add(current)
|
|
560
|
-
|
|
561
|
-
################################################################################
|
|
562
|
-
|
|
563
|
-
def untag(self, wildcard=None):
|
|
564
|
-
""" Tag all, or selected tagged items """
|
|
565
|
-
|
|
566
|
-
if wildcard:
|
|
567
|
-
remove_tags = set()
|
|
568
|
-
for entry in self.tagged_set:
|
|
569
|
-
if fnmatch.fnmatch(self.filtered_file_indices[entry], wildcard):
|
|
570
|
-
remove_tags.add(entry)
|
|
571
|
-
|
|
572
|
-
self.tagged_set -= remove_tags
|
|
573
|
-
else:
|
|
574
|
-
self.tagged_set = set()
|
|
575
|
-
|
|
576
|
-
################################################################################
|
|
577
|
-
|
|
578
|
-
def get_hidden_visibility(self):
|
|
579
|
-
""" Return the current state of hidden file visibility """
|
|
580
|
-
|
|
581
|
-
return not self.hide_hidden_filter
|
|
582
|
-
|
|
583
|
-
################################################################################
|
|
584
|
-
|
|
585
|
-
def set_hidden_visibility(self, state=False):
|
|
586
|
-
""" Set the visibility of hidden files """
|
|
587
|
-
|
|
588
|
-
self.hide_hidden_filter = not state
|
|
589
|
-
|
|
590
|
-
change_txt = 'Hiding' if self.hide_hidden_filter else 'Showing'
|
|
591
|
-
|
|
592
|
-
with popup.PopUp(self.screen, f'{change_txt} hidden files', self.colours['status']):
|
|
593
|
-
self.update_file_list()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{skilleter_modules-0.0.3 → skilleter_modules-0.0.6}/src/skilleter_modules.egg-info/requires.txt
RENAMED
|
File without changes
|
{skilleter_modules-0.0.3 → skilleter_modules-0.0.6}/src/skilleter_modules.egg-info/top_level.txt
RENAMED
|
File without changes
|