skilleter-thingy 0.3.14__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.
- skilleter_thingy/__init__.py +0 -0
- skilleter_thingy/addpath.py +107 -0
- skilleter_thingy/console_colours.py +63 -0
- skilleter_thingy/ffind.py +535 -0
- skilleter_thingy/ggit.py +88 -0
- skilleter_thingy/ggrep.py +155 -0
- skilleter_thingy/git_br.py +186 -0
- skilleter_thingy/git_ca.py +147 -0
- skilleter_thingy/git_cleanup.py +297 -0
- skilleter_thingy/git_co.py +227 -0
- skilleter_thingy/git_common.py +68 -0
- skilleter_thingy/git_hold.py +162 -0
- skilleter_thingy/git_parent.py +84 -0
- skilleter_thingy/git_retag.py +67 -0
- skilleter_thingy/git_review.py +1450 -0
- skilleter_thingy/git_update.py +398 -0
- skilleter_thingy/git_wt.py +72 -0
- skilleter_thingy/gitcmp_helper.py +328 -0
- skilleter_thingy/gitprompt.py +293 -0
- skilleter_thingy/linecount.py +154 -0
- skilleter_thingy/multigit.py +915 -0
- skilleter_thingy/py_audit.py +133 -0
- skilleter_thingy/remdir.py +127 -0
- skilleter_thingy/rpylint.py +98 -0
- skilleter_thingy/strreplace.py +82 -0
- skilleter_thingy/test.py +34 -0
- skilleter_thingy/tfm.py +948 -0
- skilleter_thingy/tfparse.py +101 -0
- skilleter_thingy/trimpath.py +82 -0
- skilleter_thingy/venv_create.py +47 -0
- skilleter_thingy/venv_template.py +47 -0
- skilleter_thingy/xchmod.py +124 -0
- skilleter_thingy/yamlcheck.py +89 -0
- skilleter_thingy-0.3.14.dist-info/METADATA +606 -0
- skilleter_thingy-0.3.14.dist-info/RECORD +39 -0
- skilleter_thingy-0.3.14.dist-info/WHEEL +5 -0
- skilleter_thingy-0.3.14.dist-info/entry_points.txt +31 -0
- skilleter_thingy-0.3.14.dist-info/licenses/LICENSE +619 -0
- skilleter_thingy-0.3.14.dist-info/top_level.txt +1 -0
skilleter_thingy/tfm.py
ADDED
|
@@ -0,0 +1,948 @@
|
|
|
1
|
+
#! /usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
################################################################################
|
|
4
|
+
""" Thingy file manager
|
|
5
|
+
|
|
6
|
+
Copyright (C) 2022 John Skilleter
|
|
7
|
+
|
|
8
|
+
Stuff not working:
|
|
9
|
+
|
|
10
|
+
BUG: 1. If both panes showing same directory, non-current pane doesn't update if current one changes
|
|
11
|
+
BUG: 2. Allows an attempt to move/copy directory into its own subdirectory
|
|
12
|
+
BUG: 2. If pane showing a subdirectory and parent is deleted in other pane nothing updates
|
|
13
|
+
BUG: 3. Inefficient redraw (better, but could be better, although curses hides most of the inefficiencies)
|
|
14
|
+
BUG: 4. Many untrapped exceptions (e.g. deleting stuff)
|
|
15
|
+
BUG: 6. Slightly odd scrolling at page top/bottom
|
|
16
|
+
|
|
17
|
+
Stuff that's been fixed but not fully tested:
|
|
18
|
+
|
|
19
|
+
FIXED: 1. If a tagged file is hidden by a filter it shouldn't be processed by move/copy/delete - check this
|
|
20
|
+
|
|
21
|
+
DONE:
|
|
22
|
+
DONE: 1. If you exit a directory via Backspace then re-enter it hasn't saved the current position
|
|
23
|
+
DONE: 1. Implement inotify as background thread as it is far too slow to run everytime we process a keypress
|
|
24
|
+
DONE: 1. When changing direcrories, save current location in current directory and return there when we return to the same directory
|
|
25
|
+
DONE: 2. Copy (file or directory)
|
|
26
|
+
DONE: 2. Move (file or directory)
|
|
27
|
+
DONE: 2. Use inotify to check for updates and update panes
|
|
28
|
+
DONE: 3. Directory delete - with warning if directory not empty) and/or count of total number of files
|
|
29
|
+
|
|
30
|
+
CONSIDERED, BUT WON'T DO:
|
|
31
|
+
NOPE: 3. Look at lfm source code to see how it does things (check licence for compatibility first) - Have looked at feature list and 'borrowed' some
|
|
32
|
+
NOPE: 8. Multiple panes (fixed at 2 to start with)
|
|
33
|
+
NOPE: 9. GUI version
|
|
34
|
+
|
|
35
|
+
TODO:
|
|
36
|
+
TODO: 1. Config file - keyboard config, override apps used to open particular file types
|
|
37
|
+
TODO: 2. '>' marker on current file or don't use background colours except for current file
|
|
38
|
+
TODO: 2. Scrolling help window
|
|
39
|
+
TODO: 2. Wildcard tagging/untagging
|
|
40
|
+
TODO: 2. ^R - redraw
|
|
41
|
+
TODO: 3. '.' show/hide hidden files
|
|
42
|
+
TODO: 3. '=' sets other pane to same directory as current one
|
|
43
|
+
TODO: 3. Allow copy/move to overwrite files (prompt first, with Y/N/A)
|
|
44
|
+
TODO: 3. Configurable shortcuts
|
|
45
|
+
TODO: 3. Create directory
|
|
46
|
+
TODO: 3. Menu for things like sort order
|
|
47
|
+
TODO: 3. Search - name or content
|
|
48
|
+
TODO: 3. Sort by date, size, owner, group, cheese preference, etc.
|
|
49
|
+
TODO: 3. Tagging - invert selection
|
|
50
|
+
TODO: 3. Use inotify and cache file data, or at least use inotify and update when current directory changes
|
|
51
|
+
TODO: 3. Use pygments/pymentize for syntax highlighting in file viewer
|
|
52
|
+
TODO: 3. When moving between panes move to same line on screen in new pane if possible
|
|
53
|
+
TODO: 4. Bookmarks for directories
|
|
54
|
+
TODO: 4. F12 = file info = show permissions, times, etc. in pop-up
|
|
55
|
+
TODO: 4. File list - proper formatting, nicer dates/times, support uid, gid, mode, size adjusted for KiB/MiB/GiB and with commas
|
|
56
|
+
TODO: 4. File search/grep
|
|
57
|
+
TODO: 4. Highlight current pane - draw border around it?
|
|
58
|
+
TODO: 4. Main loop is inefficient - we call filemanager.show_file_list() on every event
|
|
59
|
+
TODO: 4. Menu system with menu bar and pull-down menus
|
|
60
|
+
TODO: 4. Rename
|
|
61
|
+
TODO: 4. Tab to switch panes, left arrow to move up a directory level, right arrow to move into directory under cursor (if it is one)
|
|
62
|
+
TODO: 4. Treat symlinks to directories as directories not files
|
|
63
|
+
TODO: 4. ^F+string = go to first entry matching 'string'
|
|
64
|
+
TODO: 4. ^N = go to next ^F/^S match
|
|
65
|
+
TODO: 4. ^S+strign = go to first entry starting with 'string'
|
|
66
|
+
TODO: 4. ^U - Swap panes
|
|
67
|
+
TODO: 5. Built-in editor/viewer for text files (can use for help)
|
|
68
|
+
TODO: 5. Create symlink
|
|
69
|
+
TODO: 5. F11 = directory info = show number of files, size
|
|
70
|
+
TODO: 5. Option to echo cwd on exit so shell can pick up current location as pwd
|
|
71
|
+
TODO: 5. Scroll bar or per-pane status line showing current file index and number of files
|
|
72
|
+
TODO: 5. Shell window - enter command and run it in current tab
|
|
73
|
+
TODO: 5. Status at bottom of pane, tagged, hiddenm, total files & directories
|
|
74
|
+
TODO: 5. Verify it works in 16-colour terminal
|
|
75
|
+
TODO: 6. Config file - saves bookmarks, current keyboard config
|
|
76
|
+
TODO: 6. Configuration load/save - global, not directory-specific
|
|
77
|
+
TODO: 6. Control file details visible (user/group/size/permissions/times)
|
|
78
|
+
TODO: 6. F12 window with edit option to change permissions, owner
|
|
79
|
+
TODO: 6. Consider splitting TFM out from skilleter-thingy as a separate project; I don't think there's any overlap with the rest of skilleter-thingy
|
|
80
|
+
TODO: 6. File viewer
|
|
81
|
+
TODO: 6. File/directory info
|
|
82
|
+
TODO: 6. More command line parameters & options (directrories, search order, filters)
|
|
83
|
+
TODO: 6. Open/close tab
|
|
84
|
+
TODO: 6. Pickle and unpickle state
|
|
85
|
+
TODO: 6. Save settings in config file - bookmarks, key configuration, colours, etc.
|
|
86
|
+
TODO: 6. Search for file, wildcards, show list, select and move to directory
|
|
87
|
+
TODO: 6. Swap panes
|
|
88
|
+
TODO: 6. Synchronised scrolling across multiple panes
|
|
89
|
+
TODO: 6. Tree view
|
|
90
|
+
TODO: 6. Tree view for navigation
|
|
91
|
+
TODO: 6. Update progress when copying/moving/deleting
|
|
92
|
+
TODO: 7. Configurable colours
|
|
93
|
+
TODO: 7. Navigation by partial name (press prefix then start typing name and it moves to matching file)
|
|
94
|
+
TODO: 7. Use pads for panes (might be expensive in directories with lots of files)
|
|
95
|
+
TODO: 8. Allow copy/move to merge directories
|
|
96
|
+
TODO: 8. Better help
|
|
97
|
+
TODO: 8. Chmod/chown
|
|
98
|
+
TODO: 8. Directory bookmarks with selection menu
|
|
99
|
+
TODO: 8. Special environment vars in shell window - e.g. $:d for current directory $:od for current directory in other pane, $:f - current file $:of current file in other pane
|
|
100
|
+
TODO: 8. Touch file
|
|
101
|
+
TODO: 8. Trying to suspend tfm, run a command in the current shell and resume it doesn't work since I added threads for keyboard and inotify
|
|
102
|
+
TODO: 9. Git commands
|
|
103
|
+
TODO: 9. Mouse support
|
|
104
|
+
TODO: 9. multigit integration
|
|
105
|
+
|
|
106
|
+
"""
|
|
107
|
+
################################################################################
|
|
108
|
+
|
|
109
|
+
import os
|
|
110
|
+
import sys
|
|
111
|
+
import argparse
|
|
112
|
+
import curses
|
|
113
|
+
import subprocess
|
|
114
|
+
import shutil
|
|
115
|
+
from collections import defaultdict
|
|
116
|
+
import threading
|
|
117
|
+
import queue
|
|
118
|
+
|
|
119
|
+
from skilleter_modules import popup
|
|
120
|
+
from skilleter_modules import tfm_pane
|
|
121
|
+
|
|
122
|
+
################################################################################
|
|
123
|
+
# Colour pair codes
|
|
124
|
+
|
|
125
|
+
COLOUR_NORMAL = 1
|
|
126
|
+
COLOUR_STATUS = 2
|
|
127
|
+
COLOUR_BACKGROUND = 3
|
|
128
|
+
COLOUR_WARNING = 4
|
|
129
|
+
|
|
130
|
+
RESERVED_COLOURS = 5
|
|
131
|
+
|
|
132
|
+
# TODO: Find a better way of sharing colours with the tfm_pane class
|
|
133
|
+
|
|
134
|
+
PANE_COLOURS = {'normal': 1, 'status': 2, 'background': 3, 'warning': 4, 'reserved_colours': 5}
|
|
135
|
+
|
|
136
|
+
# Version - used to update old pickle data
|
|
137
|
+
|
|
138
|
+
VERSION = 1
|
|
139
|
+
|
|
140
|
+
# Minimum console window size for functionality
|
|
141
|
+
|
|
142
|
+
MIN_CONSOLE_WIDTH = 64
|
|
143
|
+
MIN_CONSOLE_HEIGHT = 16
|
|
144
|
+
|
|
145
|
+
# Number of panes (TODO: Make this variable, rather than fixed)
|
|
146
|
+
|
|
147
|
+
NUM_PANES = 2
|
|
148
|
+
|
|
149
|
+
# Function key labels
|
|
150
|
+
|
|
151
|
+
FN_KEY_FN = ('Help', 'View', 'Search', 'Edit', 'Copy', 'Move', 'Mkdir', 'Delete', 'Rename', 'Quit')
|
|
152
|
+
|
|
153
|
+
################################################################################
|
|
154
|
+
|
|
155
|
+
class FileManagerError(BaseException):
|
|
156
|
+
""" Exception for the application """
|
|
157
|
+
|
|
158
|
+
def __init__(self, msg, status=1):
|
|
159
|
+
super().__init__(msg)
|
|
160
|
+
self.msg = msg
|
|
161
|
+
self.status = status
|
|
162
|
+
|
|
163
|
+
################################################################################
|
|
164
|
+
|
|
165
|
+
def error(msg, status=1):
|
|
166
|
+
""" Report an error """
|
|
167
|
+
|
|
168
|
+
sys.stderr.write('%s\n' % msg)
|
|
169
|
+
sys.exit(status)
|
|
170
|
+
|
|
171
|
+
################################################################################
|
|
172
|
+
|
|
173
|
+
def in_directory(root, entry):
|
|
174
|
+
""" Return True if a directory lies within another """
|
|
175
|
+
|
|
176
|
+
return os.path.commonpath([root, entry]) == root
|
|
177
|
+
|
|
178
|
+
################################################################################
|
|
179
|
+
|
|
180
|
+
def pickle_filename(working_tree):
|
|
181
|
+
""" Return the name of the pickle file for this working tree """
|
|
182
|
+
|
|
183
|
+
pickle_dir = os.path.join(os.environ['HOME'], '.config', 'tfm')
|
|
184
|
+
|
|
185
|
+
if not os.path.isdir(pickle_dir):
|
|
186
|
+
os.mkdir(pickle_dir)
|
|
187
|
+
|
|
188
|
+
pickle_file = working_tree.replace('/', '~')
|
|
189
|
+
|
|
190
|
+
return os.path.join(pickle_dir, pickle_file)
|
|
191
|
+
|
|
192
|
+
################################################################################
|
|
193
|
+
|
|
194
|
+
def read_input(prompt):
|
|
195
|
+
""" Read input from the user """
|
|
196
|
+
|
|
197
|
+
win = curses.newwin(3, 60, 3, 10)
|
|
198
|
+
win.attron(curses.color_pair(COLOUR_STATUS))
|
|
199
|
+
win.box()
|
|
200
|
+
|
|
201
|
+
win.addstr(1, 1, prompt)
|
|
202
|
+
curses.curs_set(2)
|
|
203
|
+
win.refresh()
|
|
204
|
+
curses.echo()
|
|
205
|
+
text = win.getstr().decode(encoding='utf-8')
|
|
206
|
+
curses.noecho()
|
|
207
|
+
curses.curs_set(0)
|
|
208
|
+
|
|
209
|
+
return text
|
|
210
|
+
|
|
211
|
+
################################################################################
|
|
212
|
+
|
|
213
|
+
def keyboard_wait(self):
|
|
214
|
+
"""Thread to wait for keypresses and post them onto the event queue"""
|
|
215
|
+
|
|
216
|
+
while True:
|
|
217
|
+
keypress = self.screen.getch()
|
|
218
|
+
self.event_queue.put(('key', keypress))
|
|
219
|
+
|
|
220
|
+
################################################################################
|
|
221
|
+
|
|
222
|
+
class FileManager():
|
|
223
|
+
""" Review function as a class """
|
|
224
|
+
|
|
225
|
+
def __init__(self, args):
|
|
226
|
+
""" Initialisation """
|
|
227
|
+
|
|
228
|
+
_ = args
|
|
229
|
+
|
|
230
|
+
# Move to the top-level directory in the working tree
|
|
231
|
+
|
|
232
|
+
self.init_key_despatch_table()
|
|
233
|
+
|
|
234
|
+
# Initialise the screen
|
|
235
|
+
|
|
236
|
+
self.screen = curses.initscr()
|
|
237
|
+
|
|
238
|
+
# Configure the colours, set the background & hide the cursor
|
|
239
|
+
|
|
240
|
+
self.init_colors()
|
|
241
|
+
|
|
242
|
+
# See if we have saved state for this repo
|
|
243
|
+
|
|
244
|
+
self.load_state()
|
|
245
|
+
|
|
246
|
+
# Create the queue for keyboard, file events
|
|
247
|
+
|
|
248
|
+
self.event_queue = queue.Queue()
|
|
249
|
+
|
|
250
|
+
# Start the keyboard thread
|
|
251
|
+
|
|
252
|
+
keyboard_thread = threading.Thread(target=keyboard_wait, args=(self, ), daemon=True)
|
|
253
|
+
keyboard_thread.start()
|
|
254
|
+
|
|
255
|
+
# Create the panes
|
|
256
|
+
|
|
257
|
+
self.panes = []
|
|
258
|
+
|
|
259
|
+
for i in range(NUM_PANES):
|
|
260
|
+
self.panes.append(tfm_pane.Pane(i, NUM_PANES, PANE_COLOURS, self.event_queue))
|
|
261
|
+
|
|
262
|
+
self.current_pane = self.panes[0]
|
|
263
|
+
self.pane_index = 0
|
|
264
|
+
|
|
265
|
+
self.searchstring = None
|
|
266
|
+
|
|
267
|
+
# Directory history
|
|
268
|
+
|
|
269
|
+
self.directory_history = defaultdict(str)
|
|
270
|
+
|
|
271
|
+
# Get the current console dimensions
|
|
272
|
+
|
|
273
|
+
self.update_console_size()
|
|
274
|
+
|
|
275
|
+
self.finished = False
|
|
276
|
+
|
|
277
|
+
################################################################################
|
|
278
|
+
|
|
279
|
+
def init_key_despatch_table(self):
|
|
280
|
+
""" Initialise the keyboard despatch table """
|
|
281
|
+
|
|
282
|
+
# The table is indexed by the keycode and contains help and a reference to the
|
|
283
|
+
# function that is called when the key is pressed. For clarity, all the function
|
|
284
|
+
# names are prefixed with '__key_'.
|
|
285
|
+
# Note that the function key definitions should match FN_KEY_FN
|
|
286
|
+
|
|
287
|
+
self.key_despatch_table = \
|
|
288
|
+
{
|
|
289
|
+
curses.KEY_RESIZE: {'function': self.key_console_resize},
|
|
290
|
+
|
|
291
|
+
curses.KEY_UP: {'key': 'UP', 'help': 'Move up 1 line', 'function': self.key_move_up},
|
|
292
|
+
curses.KEY_DOWN: {'key': 'DOWN', 'help': 'Move down 1 line', 'function': self.key_move_down},
|
|
293
|
+
curses.KEY_LEFT: {'key': 'LEFT', 'help': 'Move to previous pane', 'function': self.key_switch_previous_pane},
|
|
294
|
+
curses.KEY_RIGHT: {'key': 'RIGHT', 'help': 'Move to next pane', 'function': self.key_switch_next_pane},
|
|
295
|
+
curses.KEY_NPAGE: {'key': 'PGDN', 'help': 'Move down by a page', 'function': self.key_move_page_down},
|
|
296
|
+
ord('\t'): {'key': 'TAB', 'help': 'Switch panes', 'function': self.key_switch_next_pane},
|
|
297
|
+
curses.KEY_PPAGE: {'key': 'PGUP', 'help': 'Move up by a page', 'function': self.key_move_page_up},
|
|
298
|
+
curses.KEY_END: {'key': 'END', 'help': 'Move to the end of the file list', 'function': self.key_move_end},
|
|
299
|
+
curses.KEY_HOME: {'key': 'HOME', 'help': 'Move to the top of the file list', 'function': self.key_move_top},
|
|
300
|
+
curses.KEY_IC: {'key': 'INS', 'help': 'Tag file/directory', 'function': self.key_tag},
|
|
301
|
+
curses.KEY_DC: {'key': 'DEL', 'help': 'Delete current or tagged files/directories', 'function': self.key_delete},
|
|
302
|
+
curses.KEY_BACKSPACE: {'key': 'BACKSPACE', 'help': 'Move to the parent directory', 'function': self.key_parent},
|
|
303
|
+
|
|
304
|
+
curses.KEY_F1: {'key': 'F1', 'help': 'Show help', 'function': self.key_show_help},
|
|
305
|
+
curses.KEY_F2: {'key': 'F2', 'help': 'View file', 'function': self.key_view_file},
|
|
306
|
+
curses.KEY_F3: {'key': 'F3', 'help': 'Search for next match', 'function': self.key_search_again},
|
|
307
|
+
curses.KEY_F4: {'key': 'F4', 'help': 'Edit file', 'function': self.key_edit_file},
|
|
308
|
+
curses.KEY_F5: {'key': 'F5', 'help': 'Copy', 'function': self.key_copy},
|
|
309
|
+
curses.KEY_F6: {'key': 'F6', 'help': 'Move', 'function': self.key_move},
|
|
310
|
+
curses.KEY_F7: {'key': 'F7', 'help': 'MkDir', 'function': self.key_mkdir},
|
|
311
|
+
curses.KEY_F8: {'key': 'F8', 'help': 'Delete', 'function': self.key_delete},
|
|
312
|
+
curses.KEY_F9: {'key': 'F9', 'help': 'Rename', 'function': self.key_rename},
|
|
313
|
+
curses.KEY_F10: {'key': 'F10', 'help': 'Quit', 'function': self.key_quit_review},
|
|
314
|
+
|
|
315
|
+
ord('\n'): {'key': 'ENTER', 'help': 'Change directory', 'function': self.key_open_file_or_directory},
|
|
316
|
+
ord(' '): {'key': 'SPACE', 'help': 'Show file details', 'function': self.key_show_file_info},
|
|
317
|
+
ord('/'): {'help': 'Search', 'function': self.key_search_file},
|
|
318
|
+
ord('F'): {'help': 'Show only files matching a wildcard', 'function': self.key_filter_in},
|
|
319
|
+
ord('f'): {'help': 'Hide files matching a wildcard', 'function': self.key_filter_out},
|
|
320
|
+
ord('.'): {'help': 'Toggle display of hidden files', 'function': self.key_toggle_hidden},
|
|
321
|
+
ord('m'): {'help': 'Tag files that match a wildcard', 'function': self.key_wild_tag},
|
|
322
|
+
ord('M'): {'help': 'Untag files that match a wildcard', 'function': self.key_wild_untag},
|
|
323
|
+
ord('c'): {'help': 'Clear tags', 'function': self.key_clear_tags},
|
|
324
|
+
ord('R'): {'help': 'Reload', 'function': self.key_reload_changes_and_reset},
|
|
325
|
+
ord('q'): {'help': 'Quit', 'function': self.key_quit_review},
|
|
326
|
+
ord('$'): {'help': 'Open shell at location of current file', 'function': self.key_open_shell},
|
|
327
|
+
ord('e'): {'help': 'Edit the current file', 'function': self.key_edit_file},
|
|
328
|
+
ord('v'): {'help': 'View the current file', 'function': self.key_view_file},
|
|
329
|
+
ord('s'): {'help': 'Cycle sort order', 'function': self.key_cycle_sort},
|
|
330
|
+
ord('S'): {'help': 'Reverse sort order', 'function': self.key_reverse_sort},
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
################################################################################
|
|
334
|
+
|
|
335
|
+
def save_state(self):
|
|
336
|
+
""" Save the current state (normally called on exit) """
|
|
337
|
+
|
|
338
|
+
pass
|
|
339
|
+
|
|
340
|
+
################################################################################
|
|
341
|
+
|
|
342
|
+
def load_state(self):
|
|
343
|
+
""" Unpickle saved state if it exists """
|
|
344
|
+
|
|
345
|
+
pass
|
|
346
|
+
|
|
347
|
+
################################################################################
|
|
348
|
+
|
|
349
|
+
def init_colors(self):
|
|
350
|
+
""" Set up the colours and initialise the display """
|
|
351
|
+
|
|
352
|
+
curses.start_color()
|
|
353
|
+
curses.use_default_colors()
|
|
354
|
+
|
|
355
|
+
curses.init_color(15, 1000, 1000, 1000)
|
|
356
|
+
|
|
357
|
+
if os.getenv('THINGY_DARK_MODE'):
|
|
358
|
+
curses.init_pair(COLOUR_NORMAL, 15, curses.COLOR_BLACK)
|
|
359
|
+
curses.init_pair(COLOUR_STATUS, 15, curses.COLOR_GREEN)
|
|
360
|
+
curses.init_pair(COLOUR_BACKGROUND, 15, curses.COLOR_BLACK)
|
|
361
|
+
curses.init_pair(COLOUR_WARNING, 15, curses.COLOR_RED)
|
|
362
|
+
else:
|
|
363
|
+
curses.init_pair(COLOUR_NORMAL, curses.COLOR_BLACK, 15)
|
|
364
|
+
curses.init_pair(COLOUR_STATUS, 15, curses.COLOR_GREEN)
|
|
365
|
+
curses.init_pair(COLOUR_BACKGROUND, curses.COLOR_BLACK, 15)
|
|
366
|
+
curses.init_pair(COLOUR_WARNING, 15, curses.COLOR_RED)
|
|
367
|
+
|
|
368
|
+
self.screen.bkgdset(' ', curses.color_pair(COLOUR_BACKGROUND))
|
|
369
|
+
|
|
370
|
+
curses.curs_set(0)
|
|
371
|
+
|
|
372
|
+
# Clear and refresh the screen for a blank canvas
|
|
373
|
+
|
|
374
|
+
self.screen.clear()
|
|
375
|
+
self.screen.refresh()
|
|
376
|
+
|
|
377
|
+
################################################################################
|
|
378
|
+
|
|
379
|
+
def centre_text(self, y_pos, color, text):
|
|
380
|
+
""" Centre text """
|
|
381
|
+
|
|
382
|
+
if len(text) >= self.width:
|
|
383
|
+
output = text[:self.width - 1]
|
|
384
|
+
else:
|
|
385
|
+
output = text
|
|
386
|
+
|
|
387
|
+
x_pos = max(0, (self.width - len(output)) // 2)
|
|
388
|
+
|
|
389
|
+
self.screen.attron(color)
|
|
390
|
+
self.screen.hline(y_pos, 0, ' ', self.width)
|
|
391
|
+
self.screen.addstr(y_pos, x_pos, output)
|
|
392
|
+
self.screen.attroff(color)
|
|
393
|
+
|
|
394
|
+
################################################################################
|
|
395
|
+
|
|
396
|
+
def draw_screen(self):
|
|
397
|
+
""" Draw the review screen """
|
|
398
|
+
|
|
399
|
+
# Render status bar
|
|
400
|
+
|
|
401
|
+
status_bar = self.current_pane.sort_type_msg()
|
|
402
|
+
|
|
403
|
+
# TODO: Each pane needs a status bar with this info on
|
|
404
|
+
# if self.out_filter or self.in_filter:
|
|
405
|
+
# status_bar = '%s, active filters: %s' % (status_bar, self.filter_description())
|
|
406
|
+
|
|
407
|
+
self.centre_text(self.status_y, curses.color_pair(COLOUR_STATUS), status_bar)
|
|
408
|
+
|
|
409
|
+
title_bar = 'Thingy File Manager'
|
|
410
|
+
|
|
411
|
+
self.centre_text(0, curses.color_pair(COLOUR_STATUS), title_bar)
|
|
412
|
+
|
|
413
|
+
fn_width = self.width // 10
|
|
414
|
+
|
|
415
|
+
self.screen.move(self.status_y + 1, 0)
|
|
416
|
+
self.screen.clrtoeol()
|
|
417
|
+
|
|
418
|
+
for fn_key in range(1, 11):
|
|
419
|
+
self.screen.attron(curses.color_pair(COLOUR_STATUS))
|
|
420
|
+
self.screen.addstr(self.status_y + 1, (fn_key - 1) * fn_width, f'F{fn_key}')
|
|
421
|
+
self.screen.attron(curses.color_pair(COLOUR_BACKGROUND))
|
|
422
|
+
self.screen.addstr(self.status_y + 1, (fn_key - 1) * fn_width + 3, f'{FN_KEY_FN[fn_key-1]}')
|
|
423
|
+
|
|
424
|
+
self.screen.refresh()
|
|
425
|
+
|
|
426
|
+
################################################################################
|
|
427
|
+
|
|
428
|
+
def update_console_size(self):
|
|
429
|
+
""" Get current screen size and set up locations in the display """
|
|
430
|
+
|
|
431
|
+
self.height, self.width = self.screen.getmaxyx()
|
|
432
|
+
|
|
433
|
+
self.status_y = self.height - 2
|
|
434
|
+
|
|
435
|
+
for pane in self.panes:
|
|
436
|
+
pane.set_pane_coords(1, 0, self.height - 3, self.width)
|
|
437
|
+
|
|
438
|
+
if self.width < MIN_CONSOLE_WIDTH or self.height < MIN_CONSOLE_HEIGHT:
|
|
439
|
+
raise FileManagerError('Console window is too small!')
|
|
440
|
+
|
|
441
|
+
################################################################################
|
|
442
|
+
|
|
443
|
+
def run_external_command(self, cmd):
|
|
444
|
+
""" Run an external command, with the current directory being that of the
|
|
445
|
+
current file and shutting down curses before running the command
|
|
446
|
+
then restarting it """
|
|
447
|
+
|
|
448
|
+
# Run the command in a separate xterm
|
|
449
|
+
|
|
450
|
+
cmd = ['xterm', '-e'] + cmd
|
|
451
|
+
|
|
452
|
+
try:
|
|
453
|
+
subprocess.run(cmd, cwd=self.current_pane.get_current_dir(), check=True)
|
|
454
|
+
except subprocess.CalledProcessError as exc:
|
|
455
|
+
return_code = exc.returncode
|
|
456
|
+
else:
|
|
457
|
+
return_code = 0
|
|
458
|
+
|
|
459
|
+
# Reload in case we've missed something
|
|
460
|
+
|
|
461
|
+
for pane in self.panes:
|
|
462
|
+
pane.reload_changes()
|
|
463
|
+
|
|
464
|
+
self.key_console_resize()
|
|
465
|
+
|
|
466
|
+
if return_code:
|
|
467
|
+
with popup.PopUp(self.screen, f'Command returned status={return_code}', COLOUR_STATUS):
|
|
468
|
+
pass
|
|
469
|
+
|
|
470
|
+
################################################################################
|
|
471
|
+
|
|
472
|
+
def move_to_dir(self, dirname):
|
|
473
|
+
"""Move to a different directory, tracking directory history"""
|
|
474
|
+
|
|
475
|
+
current_dir = self.current_pane.get_current_dir()
|
|
476
|
+
|
|
477
|
+
self.directory_history[current_dir] = os.path.basename(self.current_pane.get_current_file()['name'])
|
|
478
|
+
new_dir = os.path.abspath(os.path.join(current_dir, dirname))
|
|
479
|
+
|
|
480
|
+
self.current_pane.set_current_dir(new_dir)
|
|
481
|
+
self.current_pane.reload_changes()
|
|
482
|
+
self.current_pane.move_to_file(self.directory_history[new_dir])
|
|
483
|
+
|
|
484
|
+
################################################################################
|
|
485
|
+
|
|
486
|
+
def key_console_resize(self):
|
|
487
|
+
""" Update the screen size variables when the console window is resized """
|
|
488
|
+
|
|
489
|
+
self.update_console_size()
|
|
490
|
+
self.draw_screen()
|
|
491
|
+
self.screen.refresh()
|
|
492
|
+
|
|
493
|
+
################################################################################
|
|
494
|
+
|
|
495
|
+
def key_show_help(self):
|
|
496
|
+
""" Show help information in a pop-up window """
|
|
497
|
+
|
|
498
|
+
# Compile list of keyboard functions
|
|
499
|
+
|
|
500
|
+
helpinfo = []
|
|
501
|
+
|
|
502
|
+
for key in self.key_despatch_table:
|
|
503
|
+
if 'help' in self.key_despatch_table[key]:
|
|
504
|
+
if 'key' in self.key_despatch_table[key]:
|
|
505
|
+
keyname = self.key_despatch_table[key]['key']
|
|
506
|
+
else:
|
|
507
|
+
keyname = chr(key)
|
|
508
|
+
|
|
509
|
+
helpinfo.append('%-5s - %s' % (keyname, self.key_despatch_table[key]['help']))
|
|
510
|
+
|
|
511
|
+
helptext = '\n'.join(helpinfo)
|
|
512
|
+
|
|
513
|
+
with popup.PopUp(self.screen, helptext, COLOUR_STATUS, waitkey=True, centre=False):
|
|
514
|
+
pass
|
|
515
|
+
|
|
516
|
+
################################################################################
|
|
517
|
+
|
|
518
|
+
def key_show_file_info(self):
|
|
519
|
+
""" TODO: Show information about the current file in a pop-up window """
|
|
520
|
+
|
|
521
|
+
pass
|
|
522
|
+
|
|
523
|
+
################################################################################
|
|
524
|
+
|
|
525
|
+
def key_switch_next_pane(self):
|
|
526
|
+
""" Switch to the next pane """
|
|
527
|
+
|
|
528
|
+
self.pane_index = (self.pane_index + 1) % NUM_PANES
|
|
529
|
+
self.current_pane = self.panes[self.pane_index]
|
|
530
|
+
|
|
531
|
+
################################################################################
|
|
532
|
+
|
|
533
|
+
def get_next_pane(self):
|
|
534
|
+
""" Return the handle of the next pane """
|
|
535
|
+
|
|
536
|
+
next_pane_index = (self.pane_index + 1) % NUM_PANES
|
|
537
|
+
|
|
538
|
+
return self.panes[next_pane_index]
|
|
539
|
+
|
|
540
|
+
################################################################################
|
|
541
|
+
|
|
542
|
+
def key_switch_previous_pane(self):
|
|
543
|
+
""" Switch to the previous pane """
|
|
544
|
+
|
|
545
|
+
self.pane_index = (self.pane_index - 1) % NUM_PANES
|
|
546
|
+
self.current_pane = self.panes[self.pane_index]
|
|
547
|
+
|
|
548
|
+
################################################################################
|
|
549
|
+
|
|
550
|
+
def key_open_file_or_directory(self):
|
|
551
|
+
""" Open the current file or change directory """
|
|
552
|
+
|
|
553
|
+
current_file = self.current_pane.get_current_file()
|
|
554
|
+
|
|
555
|
+
if current_file['isdir']:
|
|
556
|
+
self.move_to_dir(current_file['name'])
|
|
557
|
+
else:
|
|
558
|
+
# TODO: Needs to work on Mac and Windows (well, Mac, anyway!)
|
|
559
|
+
self.run_external_command(['xdg-open', current_file['name']])
|
|
560
|
+
# self.open_file(self.filtered_file_indices[self.current]['name'])
|
|
561
|
+
|
|
562
|
+
################################################################################
|
|
563
|
+
|
|
564
|
+
def key_parent(self):
|
|
565
|
+
""" Move to the parent directory """
|
|
566
|
+
|
|
567
|
+
self.move_to_dir('..')
|
|
568
|
+
|
|
569
|
+
################################################################################
|
|
570
|
+
|
|
571
|
+
def key_delete(self):
|
|
572
|
+
""" Delete the current or tagged files or directories """
|
|
573
|
+
|
|
574
|
+
files_to_delete = self.current_pane.get_tagged_files()
|
|
575
|
+
|
|
576
|
+
with popup.PopUp(self.screen, f'Deleting {len(files_to_delete)} files...', COLOUR_STATUS):
|
|
577
|
+
for entry in files_to_delete:
|
|
578
|
+
if entry['isdir']:
|
|
579
|
+
shutil.rmtree(entry['name'])
|
|
580
|
+
else:
|
|
581
|
+
os.unlink(entry['name'])
|
|
582
|
+
|
|
583
|
+
self.current_pane.update_files()
|
|
584
|
+
|
|
585
|
+
################################################################################
|
|
586
|
+
|
|
587
|
+
def key_move(self):
|
|
588
|
+
""" Move the current or tagged files to the directory in the next pane """
|
|
589
|
+
|
|
590
|
+
next_pane = self.get_next_pane()
|
|
591
|
+
|
|
592
|
+
files_to_move = self.current_pane.get_tagged_files()
|
|
593
|
+
|
|
594
|
+
current_dir = self.current_pane.get_current_dir()
|
|
595
|
+
|
|
596
|
+
destination_dir = next_pane.get_current_dir()
|
|
597
|
+
|
|
598
|
+
if current_dir == destination_dir:
|
|
599
|
+
with popup.PopUp(self.screen, 'Source and destination directories are the same', COLOUR_WARNING):
|
|
600
|
+
pass
|
|
601
|
+
else:
|
|
602
|
+
for entry in files_to_move:
|
|
603
|
+
destination = os.path.join(destination_dir, os.path.basename(entry['name']))
|
|
604
|
+
if os.path.exists(destination):
|
|
605
|
+
name = os.path.basename(entry['name'])
|
|
606
|
+
|
|
607
|
+
with popup.PopUp(self.screen, f'{name} already exists in the destination directory', COLOUR_WARNING):
|
|
608
|
+
pass
|
|
609
|
+
return
|
|
610
|
+
|
|
611
|
+
with popup.PopUp(self.screen, f'Moving {len(files_to_move)} files/directories', COLOUR_STATUS):
|
|
612
|
+
for entry in files_to_move:
|
|
613
|
+
shutil.move(entry['name'], os.path.join(destination_dir, os.path.basename(entry['name'])))
|
|
614
|
+
|
|
615
|
+
self.current_pane.update_files()
|
|
616
|
+
next_pane.update_files()
|
|
617
|
+
|
|
618
|
+
################################################################################
|
|
619
|
+
|
|
620
|
+
def key_copy(self):
|
|
621
|
+
""" Copy the current or tagged files to the directory in the next pane """
|
|
622
|
+
|
|
623
|
+
next_pane = self.get_next_pane()
|
|
624
|
+
|
|
625
|
+
files_to_copy = self.current_pane.get_tagged_files()
|
|
626
|
+
|
|
627
|
+
current_dir = self.current_pane.get_current_dir()
|
|
628
|
+
|
|
629
|
+
destination_dir = next_pane.get_current_dir()
|
|
630
|
+
|
|
631
|
+
if current_dir == destination_dir:
|
|
632
|
+
with popup.PopUp(self.screen, 'Source and destination directories are the same', COLOUR_WARNING):
|
|
633
|
+
pass
|
|
634
|
+
else:
|
|
635
|
+
for entry in files_to_copy:
|
|
636
|
+
destination = os.path.join(destination_dir, os.path.basename(entry['name']))
|
|
637
|
+
if os.path.exists(destination):
|
|
638
|
+
name = os.path.basename(entry['name'])
|
|
639
|
+
|
|
640
|
+
with popup.PopUp(self.screen, f'{name} already exists in the destination directory', COLOUR_WARNING):
|
|
641
|
+
pass
|
|
642
|
+
return
|
|
643
|
+
|
|
644
|
+
with popup.PopUp(self.screen, f'Copying {len(files_to_copy)} files/directories', COLOUR_STATUS):
|
|
645
|
+
for entry in files_to_copy:
|
|
646
|
+
if entry['isdir']:
|
|
647
|
+
shutil.copytree(entry['name'], os.path.join(destination_dir, os.path.basename(entry['name'])))
|
|
648
|
+
else:
|
|
649
|
+
shutil.copy2(entry['name'], destination_dir)
|
|
650
|
+
|
|
651
|
+
self.current_pane.update_files()
|
|
652
|
+
next_pane.update_files()
|
|
653
|
+
|
|
654
|
+
################################################################################
|
|
655
|
+
|
|
656
|
+
def key_rename(self):
|
|
657
|
+
""" Rename current or tagged files or directories """
|
|
658
|
+
|
|
659
|
+
# TODO
|
|
660
|
+
pass
|
|
661
|
+
|
|
662
|
+
################################################################################
|
|
663
|
+
|
|
664
|
+
def key_quit_review(self):
|
|
665
|
+
""" Quit """
|
|
666
|
+
|
|
667
|
+
self.finished = True
|
|
668
|
+
|
|
669
|
+
################################################################################
|
|
670
|
+
|
|
671
|
+
def key_edit_file(self):
|
|
672
|
+
""" Edit the current file """
|
|
673
|
+
|
|
674
|
+
editor = os.environ.get('EDITOR', 'vim')
|
|
675
|
+
self.run_external_command([editor, os.path.basename(self.current_pane.get_current_file()['name'])])
|
|
676
|
+
|
|
677
|
+
################################################################################
|
|
678
|
+
|
|
679
|
+
def key_view_file(self):
|
|
680
|
+
""" Edit the current file
|
|
681
|
+
TODO: Write internal file viewer """
|
|
682
|
+
|
|
683
|
+
pager = os.environ.get('TFM_PAGER', os.environ.get('PAGER', 'more'))
|
|
684
|
+
self.run_external_command([pager, os.path.basename(self.current_pane.get_current_file()['name'])])
|
|
685
|
+
|
|
686
|
+
################################################################################
|
|
687
|
+
|
|
688
|
+
def key_mkdir(self):
|
|
689
|
+
""" TODO: mkdir """
|
|
690
|
+
pass
|
|
691
|
+
|
|
692
|
+
################################################################################
|
|
693
|
+
|
|
694
|
+
def key_wild_tag(self):
|
|
695
|
+
""" TODO: """
|
|
696
|
+
pass
|
|
697
|
+
|
|
698
|
+
################################################################################
|
|
699
|
+
|
|
700
|
+
def key_wild_untag(self):
|
|
701
|
+
""" TODO: """
|
|
702
|
+
pass
|
|
703
|
+
|
|
704
|
+
################################################################################
|
|
705
|
+
|
|
706
|
+
def key_clear_tags(self):
|
|
707
|
+
""" Untag everything """
|
|
708
|
+
|
|
709
|
+
self.current_pane.untag()
|
|
710
|
+
|
|
711
|
+
################################################################################
|
|
712
|
+
|
|
713
|
+
def key_open_shell(self):
|
|
714
|
+
""" Open a shell in the same directory as the current file
|
|
715
|
+
"""
|
|
716
|
+
|
|
717
|
+
self.run_external_command([os.getenv('SHELL')])
|
|
718
|
+
self.update_console_size()
|
|
719
|
+
|
|
720
|
+
################################################################################
|
|
721
|
+
|
|
722
|
+
def key_reload_changes_and_reset(self):
|
|
723
|
+
""" Reload changes and reset the review status of each file,
|
|
724
|
+
the current file and unhide reviewed files """
|
|
725
|
+
|
|
726
|
+
with popup.PopUp(self.screen, 'Reload changes & reset reviewed status', COLOUR_STATUS):
|
|
727
|
+
self.current_pane.clear_filters()
|
|
728
|
+
self.current_pane.update_files()
|
|
729
|
+
self.current_pane.move_top()
|
|
730
|
+
|
|
731
|
+
################################################################################
|
|
732
|
+
|
|
733
|
+
def key_search_file(self):
|
|
734
|
+
""" Prompt for a search string and find a match """
|
|
735
|
+
|
|
736
|
+
self.searchstring = '*' + read_input('Search for: ') + '*'
|
|
737
|
+
|
|
738
|
+
self.current_pane.search_match(self.searchstring)
|
|
739
|
+
|
|
740
|
+
################################################################################
|
|
741
|
+
|
|
742
|
+
def key_search_again(self):
|
|
743
|
+
""" Prompt for a search string if none defined then search """
|
|
744
|
+
|
|
745
|
+
if self.searchstring:
|
|
746
|
+
self.current_pane.search_next_match()
|
|
747
|
+
else:
|
|
748
|
+
self.key_search_file()
|
|
749
|
+
|
|
750
|
+
################################################################################
|
|
751
|
+
|
|
752
|
+
def key_filter_out(self):
|
|
753
|
+
""" Hide files matching a wildcard """
|
|
754
|
+
|
|
755
|
+
filter_out = read_input('Hide files matching: ')
|
|
756
|
+
|
|
757
|
+
if filter_out:
|
|
758
|
+
self.current_pane.filter_out(filter_out)
|
|
759
|
+
|
|
760
|
+
################################################################################
|
|
761
|
+
|
|
762
|
+
def key_filter_in(self):
|
|
763
|
+
""" Only show files matching a wildcard """
|
|
764
|
+
|
|
765
|
+
filter_in = read_input('Only show files matching: ')
|
|
766
|
+
|
|
767
|
+
if filter_in:
|
|
768
|
+
self.current_pane.filter_in(filter_in)
|
|
769
|
+
|
|
770
|
+
################################################################################
|
|
771
|
+
|
|
772
|
+
def key_clear_filters(self):
|
|
773
|
+
""" Clear filters """
|
|
774
|
+
|
|
775
|
+
with popup.PopUp(self.screen, 'Cleared all filters', COLOUR_STATUS):
|
|
776
|
+
self.current_pane.clear_filters()
|
|
777
|
+
|
|
778
|
+
################################################################################
|
|
779
|
+
|
|
780
|
+
def key_toggle_hidden(self):
|
|
781
|
+
""" Toggle display of hidden files """
|
|
782
|
+
|
|
783
|
+
current_state = self.current_pane.get_hidden_visibility()
|
|
784
|
+
self.current_pane.set_hidden_visibility(not current_state)
|
|
785
|
+
|
|
786
|
+
################################################################################
|
|
787
|
+
|
|
788
|
+
def key_move_down(self):
|
|
789
|
+
""" Move down 1 line """
|
|
790
|
+
|
|
791
|
+
self.current_pane.move(1)
|
|
792
|
+
|
|
793
|
+
################################################################################
|
|
794
|
+
|
|
795
|
+
def key_move_up(self):
|
|
796
|
+
""" Move up 1 line """
|
|
797
|
+
|
|
798
|
+
self.current_pane.move(-1)
|
|
799
|
+
|
|
800
|
+
################################################################################
|
|
801
|
+
|
|
802
|
+
def key_move_page_down(self):
|
|
803
|
+
""" Move down by a page """
|
|
804
|
+
|
|
805
|
+
self.current_pane.move_page_down()
|
|
806
|
+
|
|
807
|
+
################################################################################
|
|
808
|
+
|
|
809
|
+
def key_move_page_up(self):
|
|
810
|
+
""" Move up by a page """
|
|
811
|
+
|
|
812
|
+
self.current_pane.move_page_up()
|
|
813
|
+
|
|
814
|
+
################################################################################
|
|
815
|
+
|
|
816
|
+
def key_move_top(self):
|
|
817
|
+
""" Move to the top of the file list """
|
|
818
|
+
|
|
819
|
+
self.current_pane.move_top()
|
|
820
|
+
|
|
821
|
+
################################################################################
|
|
822
|
+
|
|
823
|
+
def key_move_end(self):
|
|
824
|
+
""" Move to the end of the file list """
|
|
825
|
+
|
|
826
|
+
self.current_pane.move_end()
|
|
827
|
+
|
|
828
|
+
################################################################################
|
|
829
|
+
|
|
830
|
+
def key_cycle_sort(self):
|
|
831
|
+
""" Cycle through the various sort options """
|
|
832
|
+
|
|
833
|
+
self.current_pane.set_sort_order(1)
|
|
834
|
+
|
|
835
|
+
################################################################################
|
|
836
|
+
|
|
837
|
+
def key_reverse_sort(self):
|
|
838
|
+
""" Reverse the current sort order """
|
|
839
|
+
|
|
840
|
+
self.current_pane.reverse_sort_order()
|
|
841
|
+
|
|
842
|
+
################################################################################
|
|
843
|
+
|
|
844
|
+
def key_tag(self):
|
|
845
|
+
""" Tag/Untag the current file """
|
|
846
|
+
|
|
847
|
+
self.current_pane.tag_current()
|
|
848
|
+
self.key_move_down()
|
|
849
|
+
|
|
850
|
+
################################################################################
|
|
851
|
+
|
|
852
|
+
def done(self):
|
|
853
|
+
""" Quit """
|
|
854
|
+
|
|
855
|
+
return self.finished
|
|
856
|
+
|
|
857
|
+
################################################################################
|
|
858
|
+
|
|
859
|
+
def handle_keypress(self, keypress):
|
|
860
|
+
""" Handle a key press """
|
|
861
|
+
|
|
862
|
+
if keypress in self.key_despatch_table:
|
|
863
|
+
self.key_despatch_table[keypress]['function']()
|
|
864
|
+
|
|
865
|
+
# Keep the current entry in range
|
|
866
|
+
|
|
867
|
+
self.current_pane.constrain_display_parameters()
|
|
868
|
+
|
|
869
|
+
################################################################################
|
|
870
|
+
|
|
871
|
+
def show_file_list(self):
|
|
872
|
+
""" Show all file lists """
|
|
873
|
+
|
|
874
|
+
for pane in self.panes:
|
|
875
|
+
pane.show_file_list(pane == self.current_pane)
|
|
876
|
+
|
|
877
|
+
################################################################################
|
|
878
|
+
|
|
879
|
+
def event(self):
|
|
880
|
+
"""Wait for, and return the next event from the event queue"""
|
|
881
|
+
|
|
882
|
+
return self.event_queue.get()
|
|
883
|
+
|
|
884
|
+
################################################################################
|
|
885
|
+
|
|
886
|
+
def parse_command_line():
|
|
887
|
+
""" Parse the command line, return the arguments """
|
|
888
|
+
|
|
889
|
+
# TODO: Options and arguments
|
|
890
|
+
|
|
891
|
+
parser = argparse.ArgumentParser(description='Console file manager')
|
|
892
|
+
|
|
893
|
+
parser.add_argument('--pudb', action='store_true', help='Invoke pudb debugger over Telnet')
|
|
894
|
+
|
|
895
|
+
args = parser.parse_args()
|
|
896
|
+
|
|
897
|
+
args.paths = None
|
|
898
|
+
|
|
899
|
+
return args
|
|
900
|
+
|
|
901
|
+
################################################################################
|
|
902
|
+
|
|
903
|
+
def main(screen, args):
|
|
904
|
+
""" Parse the command line and run the review """
|
|
905
|
+
|
|
906
|
+
filemanager = FileManager(args)
|
|
907
|
+
|
|
908
|
+
filemanager.draw_screen()
|
|
909
|
+
|
|
910
|
+
while not filemanager.done():
|
|
911
|
+
filemanager.show_file_list()
|
|
912
|
+
|
|
913
|
+
event, data = filemanager.event()
|
|
914
|
+
|
|
915
|
+
if event == 'key':
|
|
916
|
+
filemanager.handle_keypress(data)
|
|
917
|
+
elif event == 'inotify':
|
|
918
|
+
filemanager.panes[data].reload_changes()
|
|
919
|
+
|
|
920
|
+
filemanager.save_state()
|
|
921
|
+
|
|
922
|
+
################################################################################
|
|
923
|
+
|
|
924
|
+
def tfm():
|
|
925
|
+
"""Main function"""
|
|
926
|
+
|
|
927
|
+
try:
|
|
928
|
+
command_args = parse_command_line()
|
|
929
|
+
|
|
930
|
+
if command_args.pudb:
|
|
931
|
+
from pudb.remote import set_trace
|
|
932
|
+
set_trace(term_size=(190, 45))
|
|
933
|
+
|
|
934
|
+
curses.wrapper(main, command_args)
|
|
935
|
+
|
|
936
|
+
except KeyboardInterrupt:
|
|
937
|
+
sys.exit(1)
|
|
938
|
+
|
|
939
|
+
except BrokenPipeError:
|
|
940
|
+
sys.exit(2)
|
|
941
|
+
|
|
942
|
+
except FileManagerError as exc:
|
|
943
|
+
print(exc.msg)
|
|
944
|
+
|
|
945
|
+
################################################################################
|
|
946
|
+
|
|
947
|
+
if __name__ == '__main__':
|
|
948
|
+
tfm()
|