skilleter-thingy 0.0.40__py3-none-any.whl → 0.0.42__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.

Files changed (68) hide show
  1. skilleter_thingy/__init__.py +6 -0
  2. skilleter_thingy/addpath.py +107 -0
  3. skilleter_thingy/borger.py +269 -0
  4. skilleter_thingy/console_colours.py +63 -0
  5. skilleter_thingy/diskspacecheck.py +67 -0
  6. skilleter_thingy/docker_purge.py +113 -0
  7. skilleter_thingy/ffind.py +536 -0
  8. skilleter_thingy/ggit.py +90 -0
  9. skilleter_thingy/ggrep.py +154 -0
  10. skilleter_thingy/git_br.py +180 -0
  11. skilleter_thingy/git_ca.py +142 -0
  12. skilleter_thingy/git_cleanup.py +287 -0
  13. skilleter_thingy/git_co.py +220 -0
  14. skilleter_thingy/git_common.py +61 -0
  15. skilleter_thingy/git_hold.py +154 -0
  16. skilleter_thingy/git_mr.py +92 -0
  17. skilleter_thingy/git_parent.py +77 -0
  18. skilleter_thingy/git_review.py +1428 -0
  19. skilleter_thingy/git_update.py +385 -0
  20. skilleter_thingy/git_wt.py +96 -0
  21. skilleter_thingy/gitcmp_helper.py +322 -0
  22. skilleter_thingy/gitprompt.py +274 -0
  23. skilleter_thingy/gl.py +174 -0
  24. skilleter_thingy/gphotosync.py +610 -0
  25. skilleter_thingy/linecount.py +155 -0
  26. skilleter_thingy/moviemover.py +133 -0
  27. skilleter_thingy/photodupe.py +136 -0
  28. skilleter_thingy/phototidier.py +248 -0
  29. skilleter_thingy/py_audit.py +131 -0
  30. skilleter_thingy/readable.py +270 -0
  31. skilleter_thingy/remdir.py +126 -0
  32. skilleter_thingy/rmdupe.py +550 -0
  33. skilleter_thingy/rpylint.py +91 -0
  34. skilleter_thingy/splitpics.py +99 -0
  35. skilleter_thingy/strreplace.py +82 -0
  36. skilleter_thingy/sysmon.py +435 -0
  37. skilleter_thingy/tfm.py +920 -0
  38. skilleter_thingy/tfparse.py +101 -0
  39. skilleter_thingy/thingy/__init__.py +6 -0
  40. skilleter_thingy/thingy/colour.py +213 -0
  41. skilleter_thingy/thingy/dc_curses.py +278 -0
  42. skilleter_thingy/thingy/dc_defaults.py +221 -0
  43. skilleter_thingy/thingy/dc_util.py +50 -0
  44. skilleter_thingy/thingy/dircolors.py +308 -0
  45. skilleter_thingy/thingy/docker.py +95 -0
  46. skilleter_thingy/thingy/files.py +142 -0
  47. skilleter_thingy/thingy/git.py +1371 -0
  48. skilleter_thingy/thingy/git2.py +1307 -0
  49. skilleter_thingy/thingy/gitlab.py +193 -0
  50. skilleter_thingy/thingy/logger.py +112 -0
  51. skilleter_thingy/thingy/path.py +156 -0
  52. skilleter_thingy/thingy/popup.py +87 -0
  53. skilleter_thingy/thingy/process.py +112 -0
  54. skilleter_thingy/thingy/run.py +334 -0
  55. skilleter_thingy/thingy/tfm_pane.py +595 -0
  56. skilleter_thingy/thingy/tidy.py +160 -0
  57. skilleter_thingy/trimpath.py +84 -0
  58. skilleter_thingy/window_rename.py +92 -0
  59. skilleter_thingy/xchmod.py +125 -0
  60. skilleter_thingy/yamlcheck.py +89 -0
  61. {skilleter_thingy-0.0.40.dist-info → skilleter_thingy-0.0.42.dist-info}/METADATA +5 -1
  62. skilleter_thingy-0.0.42.dist-info/RECORD +66 -0
  63. {skilleter_thingy-0.0.40.dist-info → skilleter_thingy-0.0.42.dist-info}/entry_points.txt +1 -0
  64. skilleter_thingy-0.0.42.dist-info/top_level.txt +1 -0
  65. skilleter_thingy-0.0.40.dist-info/RECORD +0 -6
  66. skilleter_thingy-0.0.40.dist-info/top_level.txt +0 -1
  67. {skilleter_thingy-0.0.40.dist-info → skilleter_thingy-0.0.42.dist-info}/LICENSE +0 -0
  68. {skilleter_thingy-0.0.40.dist-info → skilleter_thingy-0.0.42.dist-info}/WHEEL +0 -0
@@ -0,0 +1,595 @@
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
+ import thingy.dc_curses as dc_curses
20
+ import thingy.path as path
21
+ import thingy.popup as 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 = None
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
+ else:
294
+ filename = entry = None
295
+ current = False
296
+
297
+ # Render the current line
298
+
299
+ file_colour = self.dircolours.get_colour_pair(current_file['name'], current_file['mode']) if filename else normal_colour
300
+
301
+ # Reverse the colours if this the cursor line
302
+
303
+ if current and current_pane:
304
+ file_colour |= curses.A_REVERSE
305
+ normal_colour |= curses.A_REVERSE
306
+
307
+ # Write the prefix, filename, and, if necessary, padding
308
+
309
+ self.screen.move(self.file_list_y + ypos, 0)
310
+ if entry:
311
+ self.screen.addstr(entry, file_colour)
312
+ else:
313
+ self.screen.clrtoeol()
314
+
315
+ #if len(filename) < self.width:
316
+ # self.screen.addstr(self.file_list_y + ypos, len(filename), ' ' * (self.width - len(filename)), normal_colour)
317
+
318
+ current_dir = path.trimpath(self.current_dir, self.width)
319
+
320
+ self.screen.move(0, 0)
321
+ self.screen.attron(curses.color_pair(self.colours['status']))
322
+ self.screen.addstr(current_dir + ' '*(self.width-len(current_dir)))
323
+
324
+ self.screen.refresh()
325
+
326
+ if not self.filtered_file_indices:
327
+ with popup.PopUp(self.screen, 'All files are hidden - Press \'c\' to clear filters.', self.colours['status']):
328
+ pass
329
+
330
+ ################################################################################
331
+
332
+ def filter_description(self):
333
+ """ Return a textual description of the active filters """
334
+
335
+ filters = []
336
+
337
+ if self.out_filter:
338
+ filters.append('filter-out wildcard')
339
+
340
+ if self.in_filter:
341
+ filters.append('filter-in wildcard')
342
+
343
+ return ', '.join(filters)
344
+
345
+ ################################################################################
346
+
347
+ def clear_filters(self):
348
+ """ Clear all filters """
349
+
350
+ if self.out_filter or self.in_filter:
351
+ self.out_filter = self.in_filter = None
352
+ self.update_file_list()
353
+
354
+ ################################################################################
355
+
356
+ def reload_changes(self):
357
+ """ Update the list of files in case something external has
358
+ changed it. """
359
+
360
+ self.update_files()
361
+
362
+ ################################################################################
363
+
364
+ def get_current_dir(self):
365
+ """ Get the current directory for the pane """
366
+
367
+ return self.current_dir
368
+
369
+ ################################################################################
370
+
371
+ def set_current_dir(self, directory):
372
+ """ Set the current directory for the pane """
373
+
374
+ if self.current_dir and self.ino:
375
+ self.ino.remove_watch(self.current_dir)
376
+
377
+ self.current_dir = os.path.normpath(directory)
378
+
379
+ if self.ino:
380
+ self.ino.add_watch(directory)
381
+
382
+ ################################################################################
383
+
384
+ def get_current_file(self):
385
+ """ Get the current file for the pane """
386
+
387
+ return self.file_list[self.filtered_file_indices[self.current]]
388
+
389
+ ################################################################################
390
+
391
+ def get_tagged_files(self):
392
+ """ Get the list of tagged files, or the current file if none are tagged """
393
+
394
+ if self.tagged_set:
395
+ return [self.file_list[entry] for entry in self.filtered_file_indices if self.file_list[entry]['name'] in self.tagged_set]
396
+
397
+ return [self.get_current_file()]
398
+
399
+ ################################################################################
400
+
401
+ def search_entry(self, searchstring):
402
+ """ Search for the next match with the specified search string """
403
+
404
+ for i in list(range(self.current + 1, len(self.filtered_file_indices))) + list(range(0, self.current)):
405
+ if fnmatch.fnmatch(os.path.basename(self.file_list[self.filtered_file_indices[i]]['name']), searchstring):
406
+ self.current = i
407
+ break
408
+
409
+ ################################################################################
410
+
411
+ def search_match(self, searchstring):
412
+ """ Search for the first match """
413
+
414
+ self.searchstring = searchstring
415
+ self.search_next_match()
416
+
417
+ ################################################################################
418
+
419
+ def search_next_match(self):
420
+ """ Search for the next match with the current search string """
421
+
422
+ self.search_entry(self.searchstring)
423
+
424
+ ################################################################################
425
+
426
+ def move_end(self):
427
+ """ Move to the end of the file list """
428
+
429
+ self.current = len(self.filtered_file_indices) - 1
430
+
431
+ ################################################################################
432
+
433
+ def move_top(self):
434
+ """ Move to the top of the file list """
435
+
436
+ self.current = self.offset = 0
437
+
438
+ ################################################################################
439
+
440
+ def move_to_file(self, filename):
441
+ """ Move to the specified file (if it exists) in the current directory
442
+ or to the top if not """
443
+
444
+ self.current = self.offset = 0
445
+ if filename:
446
+ self.search_entry(filename)
447
+
448
+ ################################################################################
449
+
450
+ def move(self, delta):
451
+ """ Move up or down the file list """
452
+
453
+ self.current += delta
454
+
455
+ ################################################################################
456
+
457
+ def filter_out(self, filter_out):
458
+ """ Set an exclusion filter """
459
+
460
+ self.out_filter = filter_out
461
+ self.in_filter = None
462
+ self.update_file_list()
463
+
464
+ ################################################################################
465
+
466
+ def filter_in(self, filter_in):
467
+ """ Set an inclusion filter """
468
+
469
+ self.in_filter = filter_in
470
+ self.out_filter = None
471
+ self.update_file_list()
472
+
473
+ ################################################################################
474
+
475
+ def move_page_down(self):
476
+ """ Page down """
477
+
478
+ pos = self.current - self.offset
479
+ self.offset += self.file_list_h - 1
480
+ self.current = self.offset + pos
481
+
482
+ ################################################################################
483
+
484
+ def move_page_up(self):
485
+ """ Page up """
486
+
487
+ pos = self.current - self.offset
488
+ self.offset -= self.file_list_h - 1
489
+ self.current = self.offset + pos
490
+
491
+ ################################################################################
492
+
493
+ def set_sort_order(self, value):
494
+ """ Set the sort order """
495
+
496
+ self.sort_order = (self.sort_order + value) % SortOrder.NUM_SORTS
497
+
498
+ self.update_sort()
499
+
500
+ ################################################################################
501
+
502
+ def get_sort_order(self):
503
+ """ Get the current sort order """
504
+
505
+ return self.sort_order
506
+
507
+ ################################################################################
508
+
509
+ def sort_type_msg(self):
510
+ """ Return a textual explanation of the current sort type """
511
+
512
+ if self.reverse_sort:
513
+ msg = f'Reverse-sorting by {SORT_TYPE[self.sort_order]}'
514
+ else:
515
+ msg = f'Sorting by {SORT_TYPE[self.sort_order]}'
516
+
517
+ return msg
518
+
519
+ ################################################################################
520
+
521
+ def reverse_sort_order(self):
522
+ """ Reverse the sort order """
523
+
524
+ self.reverse_sort = not self.reverse_sort
525
+ self.update_sort()
526
+
527
+ ################################################################################
528
+
529
+ def update_sort(self):
530
+ """ Update the sort """
531
+
532
+ msg = self.sort_type_msg()
533
+
534
+ with popup.PopUp(self.screen, msg, self.colours['status']):
535
+ self.update_file_list()
536
+
537
+ ################################################################################
538
+
539
+ def set_pane_coords(self, y, x, height, width):
540
+ """ Set the pane height given the pane display area """
541
+
542
+ pane_width = width//self.num_panes
543
+
544
+ self.height = height
545
+ self.file_list_h = height-1
546
+ self.width = pane_width-1 # TODO: Why '-1'?
547
+ self.screen.resize(height, pane_width)
548
+ self.screen.mvwin(y, x + pane_width*self.index)
549
+
550
+ ################################################################################
551
+
552
+ def tag_current(self):
553
+ """ Tag the current entry (unless it is '..') """
554
+
555
+ current = self.file_list[self.filtered_file_indices[self.current]]['name']
556
+
557
+ if current != '..':
558
+ if current in self.tagged_set:
559
+ self.tagged_set.remove(current)
560
+ else:
561
+ self.tagged_set.add(current)
562
+
563
+ ################################################################################
564
+
565
+ def untag(self, wildcard=None):
566
+ """ Tag all, or selected tagged items """
567
+
568
+ if wildcard:
569
+ remove_tags = set()
570
+ for entry in self.tagged_set:
571
+ if fnmatch.fnmatch(self.filtered_file_indices[entry], wildcard):
572
+ remove_tags.add(entry)
573
+
574
+ self.tagged_set -= remove_tags
575
+ else:
576
+ self.tagged_set = set()
577
+
578
+ ################################################################################
579
+
580
+ def get_hidden_visibility(self):
581
+ """ Return the current state of hidden file visibility """
582
+
583
+ return not self.hide_hidden_filter
584
+
585
+ ################################################################################
586
+
587
+ def set_hidden_visibility(self, state=False):
588
+ """ Set the visibility of hidden files """
589
+
590
+ self.hide_hidden_filter = not state
591
+
592
+ change_txt = 'Hiding' if self.hide_hidden_filter else 'Showing'
593
+
594
+ with popup.PopUp(self.screen, f'{change_txt} hidden files', self.colours['status']):
595
+ self.update_file_list()