xefm 1.0.1__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.
Files changed (57) hide show
  1. xefm/__init__.py +27 -0
  2. xefm/__main__.py +7 -0
  3. xefm/_config.py +592 -0
  4. xefm/app.py +4599 -0
  5. xefm/archive.py +2148 -0
  6. xefm/backend_detector.py +109 -0
  7. xefm/background_shaders.py +3788 -0
  8. xefm/batch_rename_dialog.py +321 -0
  9. xefm/candidate_list.py +170 -0
  10. xefm/colors.py +754 -0
  11. xefm/compare_dialog.py +386 -0
  12. xefm/compare_selection.py +182 -0
  13. xefm/completion.py +263 -0
  14. xefm/config.py +984 -0
  15. xefm/const.py +48 -0
  16. xefm/dialog_geometry.py +149 -0
  17. xefm/diff_viewer.py +599 -0
  18. xefm/directory_diff_viewer.py +1612 -0
  19. xefm/external_programs.py +429 -0
  20. xefm/file_list_manager.py +630 -0
  21. xefm/file_monitor_manager.py +661 -0
  22. xefm/file_monitor_observer.py +416 -0
  23. xefm/file_operations.py +737 -0
  24. xefm/file_pane.py +705 -0
  25. xefm/filter_list_dialog.py +307 -0
  26. xefm/image_viewer.py +576 -0
  27. xefm/input_dialog.py +413 -0
  28. xefm/isearch_bar.py +246 -0
  29. xefm/log_manager.py +727 -0
  30. xefm/logging_handlers.py +361 -0
  31. xefm/pane_manager.py +339 -0
  32. xefm/path.py +1578 -0
  33. xefm/progress_animator.py +190 -0
  34. xefm/progress_manager.py +323 -0
  35. xefm/progressive_search_dialog.py +429 -0
  36. xefm/s3.py +1440 -0
  37. xefm/ssh.py +670 -0
  38. xefm/ssh_cache.py +330 -0
  39. xefm/ssh_config.py +140 -0
  40. xefm/ssh_connection.py +1488 -0
  41. xefm/state_manager.py +894 -0
  42. xefm/str_format.py +217 -0
  43. xefm/task.py +383 -0
  44. xefm/text_dialog.py +260 -0
  45. xefm/text_layout.py +1429 -0
  46. xefm/text_viewer.py +1232 -0
  47. xefm/tools/bcompare_dirs.py +63 -0
  48. xefm/tools/bcompare_files.py +101 -0
  49. xefm/tools/kiro.py +129 -0
  50. xefm/tools/vscode.py +134 -0
  51. xefm/viewer_registry.py +130 -0
  52. xefm-1.0.1.dist-info/METADATA +463 -0
  53. xefm-1.0.1.dist-info/RECORD +57 -0
  54. xefm-1.0.1.dist-info/WHEEL +5 -0
  55. xefm-1.0.1.dist-info/entry_points.txt +2 -0
  56. xefm-1.0.1.dist-info/licenses/LICENSE +35 -0
  57. xefm-1.0.1.dist-info/top_level.txt +1 -0
xefm/__init__.py ADDED
@@ -0,0 +1,27 @@
1
+ """
2
+ XeFM - a dual-pane file manager for the desktop and the terminal
3
+
4
+ Runs as a native desktop app on Windows and macOS and as a terminal (TUI) app on
5
+ Windows, macOS and Linux — the same widget code on either, chosen with
6
+ ``--backend``. Supports local and remote filesystems (S3, SFTP, archives).
7
+
8
+ ``xefm.app`` is the entry module (``python -m xefm`` and the ``xefm`` console
9
+ script both land in ``xefm.app.main``); its siblings hold the storage-agnostic
10
+ business logic.
11
+ """
12
+
13
+ #: Single source of truth for the version string -- the ONLY place the literal
14
+ #: appears in this repo. Bumping the version means editing this line and nothing
15
+ #: else; every consumer derives it:
16
+ #:
17
+ #: * pyproject.toml -- dynamic ``version`` (``attr = "xefm.__version__"``)
18
+ #: * xefm.app / .const -- re-export it (``--version`` output)
19
+ #: * macos_app/build.sh, macos_app/create_dmg.sh -- ``sed`` this literal out
20
+ #: * windows_app/build.ps1 -- ``Select-String`` this literal out (-> XeFM.rc)
21
+ #: * test/test_argparse_integration.py -- imports it, asserts no literal
22
+ #:
23
+ #: Don't reintroduce a copy: prose docs point here instead of restating the
24
+ #: number, and the bundle builders accept a ``VERSION`` override for one-off
25
+ #: builds without editing anything.
26
+ __version__ = "1.0.1"
27
+ __author__ = "Tomonori Shimomura"
xefm/__main__.py ADDED
@@ -0,0 +1,7 @@
1
+ """``python -m xefm`` entry point — defers to the same ``main`` the ``xefm``
2
+ console script and the macOS/Windows bundle launchers call."""
3
+
4
+ from xefm.app import main
5
+
6
+ if __name__ == "__main__":
7
+ main()
xefm/_config.py ADDED
@@ -0,0 +1,592 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ XeFM User Configuration
4
+
5
+ This file contains your personal XeFM configuration.
6
+ You can modify any of these settings to customize XeFM behavior.
7
+ """
8
+
9
+ import platform
10
+ import sys
11
+
12
+ # Import xefm_tool function and xefm_python variable for external program configuration
13
+ from xefm.external_programs import xefm_tool, xefm_python
14
+
15
+ # Import backend detector for runtime backend detection
16
+ from xefm.backend_detector import is_desktop_mode
17
+
18
+ class Config:
19
+ """User configuration for XeFM"""
20
+
21
+ # --- Desktop (GUI) mode fonts (ignored in TUI mode) ----------------------
22
+ # UI_FONT_NAME : proportional default face (file names, labels,
23
+ # dialogs, markdown prose).
24
+ # MONO_FONT_NAME : monospaced face for aligned content (size/date
25
+ # columns, viewer, diffs); also grounds the layout
26
+ # grid, so it must be monospaced.
27
+ # FONT_SIZE : point size applied to BOTH faces.
28
+ # Missing glyphs use the OS's native font substitution.
29
+ #
30
+ # None = the OS system default face -- already a matched pair per platform:
31
+ # macOS -> San Francisco + SF Mono
32
+ # Windows -> Segoe UI + Consolas
33
+ #
34
+ # To use named fonts, uncomment ONE block below (it runs after the defaults
35
+ # and overrides them). `sys` is already imported at the top of this file.
36
+
37
+ # Default: the system pair on every platform (recommended).
38
+ UI_FONT_NAME = None
39
+ MONO_FONT_NAME = None
40
+
41
+ # Example -- a sans-serif pairing:
42
+ # if sys.platform == 'darwin': # macOS
43
+ # UI_FONT_NAME = 'Helvetica Neue'
44
+ # MONO_FONT_NAME = 'Menlo'
45
+ # elif sys.platform == 'win32': # Windows
46
+ # UI_FONT_NAME = 'Segoe UI'
47
+ # MONO_FONT_NAME = 'Consolas'
48
+ # else: # other
49
+ # UI_FONT_NAME = None
50
+ # MONO_FONT_NAME = None
51
+
52
+ # Example -- a serif pairing (serif UI + serif/slab monospace):
53
+ # if sys.platform == 'darwin': # macOS
54
+ # UI_FONT_NAME = 'Georgia'
55
+ # MONO_FONT_NAME = 'PT Mono'
56
+ # elif sys.platform == 'win32': # Windows
57
+ # UI_FONT_NAME = 'Georgia'
58
+ # MONO_FONT_NAME = 'Courier New'
59
+ # else: # other
60
+ # UI_FONT_NAME = None
61
+ # MONO_FONT_NAME = None
62
+
63
+ FONT_SIZE = 12 # point size for both faces (8-72)
64
+
65
+ # Display settings
66
+ SHOW_HIDDEN_FILES = False
67
+ DEFAULT_LEFT_PANE_RATIO = 0.5 # 0.1 to 0.9
68
+ DEFAULT_LOG_HEIGHT_RATIO = 0.25 # 0.1 to 0.5
69
+ DATE_FORMAT = 'short' # 'short' (YY-MM-DD HH:mm) or 'full' (YYYY-MM-DD HH:mm:ss)
70
+
71
+ # Sorting settings
72
+ DEFAULT_SORT_MODE = 'name' # 'name', 'size', 'date'
73
+ DEFAULT_SORT_REVERSE = False
74
+
75
+ # -----------------------------------------------------------------------
76
+ # Custom themes (optional)
77
+ # -----------------------------------------------------------------------
78
+ # Register your own named themes here. Each one is added to the theme picker
79
+ # (View > Theme) and the T-key cycle alongside the built-ins — Dark+, Monokai,
80
+ # Dracula, Nord, Solarized, Gruvbox Dark, Light+, Solarized Light — so you can
81
+ # switch between them at run time. XeFM starts on Dark+ and remembers whichever
82
+ # theme you last switched to across restarts.
83
+ #
84
+ # THEMES maps a display name to a dict of color overrides. A theme inherits a
85
+ # base and overrides only what differs: set 'base' to any built-in (or another
86
+ # theme you defined above) to inherit it; with no 'base' it builds on the theme
87
+ # of the same name if one exists (so {'Dark+': {...}} tweaks the built-in), else
88
+ # on 'Dark+'. A name matching an existing theme replaces it in place.
89
+ #
90
+ # Every available key (all optional). Colors are (R, G, B) tuples, 0-255:
91
+ #
92
+ # 'base': 'Dark+' # theme to inherit (see above for default)
93
+ # # --- base palette ---
94
+ # 'background': (30, 30, 30) # content surface / editor background
95
+ # 'foreground': (212, 212, 212) # primary text
96
+ # 'muted': (157, 157, 157) # secondary text, dividers
97
+ # 'accent': (0, 122, 204) # focus ring, selection fill, default bars
98
+ # 'accent2': (78, 201, 176) # secondary accent (i-search base, recipes)
99
+ # 'surface': (48, 48, 52) # raised panels (pane header / popup)
100
+ # 'selection': (10, 105, 178) # active selection fill
101
+ # # --- chrome bars (a solid color for the whole bar) ---
102
+ # 'status': (0, 122, 204) # bottom status bar (also the viewers')
103
+ # 'footer': (0, 122, 204) # per-pane info bar
104
+ # # --- file panes: per-type name colors (a sub-dict, like 'syntax';
105
+ # # override only the types you name) ---
106
+ # 'file_types': {'directory': (204, 204, 120), # dirs (default: soft yellow)
107
+ # 'file': (212, 212, 212), # files (default: foreground)
108
+ # 'link': (86, 194, 214)} # symlinks (default: cyan)
109
+ # # ('directory' may also be given as a flat top-level key — shorthand for
110
+ # # file_types['directory']. A symlink is colored as a link even when it
111
+ # # points at a directory.)
112
+ # # --- file pane cursor cue (a sub-dict; the row outline / [ ] bracket,
113
+ # # distinct from the selection fill) ---
114
+ # 'cursor': {'active': (231, 76, 76), # focused pane (default: red)
115
+ # 'inactive': (140, 92, 94)} # blurred pane (default: muted red)
116
+ # # --- incremental search ---
117
+ # 'isearch_match': (78, 201, 176) # match-highlight base (default: accent2)
118
+ # # --- text / diff viewer syntax colors (override only the tokens you name) ---
119
+ # 'syntax': {'keyword': (86, 156, 214), 'string': (206, 145, 120),
120
+ # 'comment': (106, 153, 85), 'number': (181, 206, 168),
121
+ # 'operator': (212, 212, 212), 'builtin': (78, 201, 176),
122
+ # 'name': (156, 220, 254)}
123
+ # # --- recommended post-processing effect (GUI backend only) ---
124
+ # # A full-screen CRT / phosphor "look" composited over the rendered
125
+ # # frame. XeFM turns it on when this theme becomes active and off when you
126
+ # # switch away. Only the GUI backend (`xefm/app.py --backend gui`) renders it;
127
+ # # a terminal has no pixels to filter and silently ignores it.
128
+ # # 'post_effect': 'crt' # preset: glow + bloom + scanlines + vignette + roll
129
+ # # 'post_effect': {'bloom': 0.3, 'vignette': 0.15, 'glow': 0.22,
130
+ # # 'scanline': 0.15, 'roll': 0.1} # custom (override any)
131
+ # # --- background behind the UI (GUI backend only) ---
132
+ # # One background of two kinds (else the plain theme color). On/off with the
133
+ # # theme, like post_effect; a terminal has no pixels and ignores it. NOTE the
134
+ # # 'background' key above is the base *color* — these choose the content:
135
+ # # * animation — a slow moving scene, anchored on this theme's own colors
136
+ # # (foreground for the scene, background for the backdrop) so it stays
137
+ # # on-palette:
138
+ # # 'starfield' stars streaming toward you, fading in with depth
139
+ # # 'rain' falling streaks with fading tails
140
+ # # 'constellation' drifting nodes linked to their near neighbours
141
+ # # 'grid' flying through a wireframe corridor, the camera
142
+ # # slowly drifting and turning as it goes
143
+ # # 'wave' a dense particle wave with its own colour gradient
144
+ # # Written as a bare type, or a dict to retune speed / opacity:
145
+ # # 'animation': 'starfield' # the tuned default
146
+ # # 'animation': {'type': 'rain', 'speed': 1.0, 'opacity': 0.8}
147
+ # # ('cube', a spinning wireframe, also works — it is the UI toolkit's
148
+ # # own reference scene rather than one of XeFM's.)
149
+ # # * wallpaper — a single image scaled to fill the window:
150
+ # # 'wallpaper': '~/Pictures/bg.png'
151
+ # # 'wallpaper': {'image': '~/bg.png', 'fit': 'fit', 'opacity': 0.8}
152
+ # # fit: 'fill' (cover, default) | 'fit' (contain) | 'stretch' | 'center'
153
+ # # --- surface opacity (GUI backend only) ---
154
+ # # How opaque the UI's pane/row backgrounds are (0..1); below 1 the
155
+ # # background behind them shows through. A single per-theme value, separate
156
+ # # from the background so it applies to any kind. 1 = fully opaque UI.
157
+ # # 'opacity': 0.6
158
+ #
159
+ # Example:
160
+ #
161
+ # THEMES = {
162
+ # 'Ocean': { # builds on Dark+
163
+ # 'accent': (38, 139, 210),
164
+ # 'file_types': {'directory': (120, 200, 220), 'link': (90, 200, 180)},
165
+ # 'syntax': {'keyword': (0, 175, 215)},
166
+ # },
167
+ # 'Paper': { # a light theme, from a light base
168
+ # 'base': 'Light+',
169
+ # 'file_types': {'directory': (150, 110, 0)},
170
+ # },
171
+ # }
172
+ THEMES = {
173
+ # Phosphor: a monochrome phosphor-green CRT terminal — every color is a
174
+ # shade of green on a near-black screen. A ready-made example of a full
175
+ # custom theme; select it from View > Theme or with the T key. On the GUI
176
+ # backend the 'post_effect' below adds a real CRT glow over the green.
177
+ 'Phosphor': {
178
+ 'post_effect': 'crt', # CRT glow/bloom/scanlines (GUI backend)
179
+ 'animation': 'rain', # falling phosphor streaks (GUI backend)
180
+ 'opacity': 0.6, # chrome opacity; < 1 lets the rain show through
181
+ 'background': (4, 15, 7), # dark CRT green-black
182
+ 'foreground': (51, 245, 121), # phosphor green
183
+ 'muted': (33, 138, 74), # dim green (secondary text / dividers)
184
+ 'accent': (60, 235, 122), # focus ring / selection accent
185
+ 'accent2': (124, 255, 168), # pale mint (i-search match base)
186
+ 'surface': (11, 38, 20), # raised panels (header / popup)
187
+ 'selection': (24, 105, 54), # active selection fill
188
+ 'status': (12, 40, 22), # status bar (dark green panel)
189
+ 'footer': (22, 68, 40), # per-pane info bar (lighter, so the
190
+ # footer/status boundary reads on TUI)
191
+ 'file_types': {
192
+ 'directory': (150, 255, 150), # directories (brightest green)
193
+ 'link': (124, 255, 168), # symlinks (pale mint)
194
+ },
195
+ 'cursor': { # keep the cue on-palette, not red
196
+ 'active': (180, 255, 180), # bright green frame (focused pane)
197
+ 'inactive': (60, 150, 90), # dim green frame (blurred pane)
198
+ },
199
+ 'syntax': {
200
+ 'keyword': (130, 255, 150),
201
+ 'string': (90, 220, 120),
202
+ 'comment': (36, 140, 78),
203
+ 'number': (150, 255, 130),
204
+ 'operator': (70, 210, 110),
205
+ 'builtin': (150, 255, 170),
206
+ 'name': (60, 235, 120),
207
+ },
208
+ },
209
+ }
210
+
211
+ # Behavior settings
212
+ CONFIRM_DELETE = True # Show confirmation dialog before deleting files/directories
213
+ CONFIRM_QUIT = True # Show confirmation dialog before quitting XeFM
214
+ CONFIRM_COPY = True # Show confirmation dialog before copying files/directories
215
+ CONFIRM_MOVE = True # Show confirmation dialog before moving files/directories
216
+ CONFIRM_DUPLICATE = True # Show confirmation dialog before duplicating files/directories
217
+ CONFIRM_EXTRACT_ARCHIVE = True # Show confirmation dialog before extracting archives
218
+ CONFIRM_ARCHIVE_CREATE = True # Show confirmation dialog before creating archives
219
+
220
+ # Key bindings - customize your shortcuts
221
+ # Each action can have multiple keys assigned to it
222
+ #
223
+ # Supported formats:
224
+ # 1. Simple format: 'action': ['key1', 'key2']
225
+ # - Works regardless of selection status
226
+ # - Keys can be characters ('a', 'Q') or special key names ('HOME', 'END')
227
+ #
228
+ # 2. Extended format: 'action': {'keys': ['key1', 'key2'], 'selection': 'any|required|none'}
229
+ # - 'any': works regardless of selection status (default)
230
+ # - 'required': only works when at least one item is explicitly selected
231
+ # - 'none': only works when no items are explicitly selected
232
+ #
233
+ # Special key names (use these strings in the keys list):
234
+ # 'HOME', 'END', 'PPAGE', 'NPAGE', 'UP', 'DOWN',
235
+ # 'LEFT', 'RIGHT', 'BACKSPACE', 'DELETE', 'INSERT',
236
+ # 'F1' through 'F12'
237
+ #
238
+ KEY_BINDINGS = {
239
+ # === Application Control ===
240
+ 'quit': ['Q'], # Exit XeFM application
241
+ 'help': ['?'], # Show help dialog with all key bindings
242
+ 'redraw': ['F5'], # Additional redraw trigger (Ctrl-L is always hardcoded)
243
+
244
+ # === Navigation ===
245
+ 'cursor_up': ['UP'], # Move cursor up one item
246
+ 'cursor_down': ['DOWN'], # Move cursor down one item
247
+ 'page_up': ['PAGE_UP'], # Move cursor up one page
248
+ 'page_down': ['PAGE_DOWN'], # Move cursor down one page
249
+ 'open_item': ['ENTER'], # Open file/directory or enter directory
250
+ 'open_with_os': ['Command-ENTER'], # Open file(s) with OS default application
251
+ 'reveal_in_os': ['Alt-ENTER'], # Reveal focused file in OS file manager
252
+ 'go_parent': ['BACKSPACE'], # Go to parent directory
253
+ 'switch_pane': ['TAB'], # Switch between left and right panes
254
+ 'nav_left': ['LEFT'], # Left pane: go to parent, Right pane: switch to left pane
255
+ 'nav_right': ['RIGHT'], # Right pane: go to parent, Left pane: switch to right pane
256
+
257
+ # === File Selection ===
258
+ 'select_file': ['SPACE'], # Toggle selection of current file
259
+ 'select_file_up': ['Shift-SPACE'], # Toggle selection and move up
260
+ 'select_all': ['HOME'], # Select all items (Home key)
261
+ 'unselect_all': ['END'], # Unselect all items (End key)
262
+ 'select_all_files': ['A'], # Toggle selection of all files in current pane
263
+ 'select_all_items': ['Shift-A'], # Toggle selection of all items (files + dirs)
264
+
265
+ # === Clipboard (copy names/paths to the system clipboard) ===
266
+ 'copy_names': ['Command-Shift-C'], # Copy selected/focused file name(s) to clipboard
267
+ 'copy_paths': ['Command-Shift-P'], # Copy selected/focused full path(s) to clipboard
268
+
269
+ # === File Operations ===
270
+ 'copy_files': {'keys': ['C'], 'selection': 'required'}, # Copy selected files to other pane
271
+ 'move_files': {'keys': ['M'], 'selection': 'required'}, # Move selected files to other pane
272
+ 'delete_files': {'keys': ['K', 'DELETE'], 'selection': 'required'}, # Delete selected files/directories
273
+ 'rename_file': ['R'], # Rename selected file/directory
274
+ 'create_file': ['Shift-E'], # Create new file (prompts for filename)
275
+ 'create_directory': {'keys': ['M'], 'selection': 'none'}, # Create new directory (only when no files selected)
276
+
277
+ # === File Viewing & Editing ===
278
+ 'view_file': ['V'], # View file using configured viewer
279
+ 'edit_file': ['E'], # Edit selected file with configured text editor
280
+ 'file_details': ['I'], # Show detailed file information dialog
281
+
282
+ # === File Comparison ===
283
+ 'diff_files': ['EQUAL'], # Compare two selected files side-by-side
284
+ 'diff_directories': ['Shift-EQUAL'], # Compare directories recursively
285
+
286
+ # === Archive Operations ===
287
+ 'create_archive': {'keys': ['P'], 'selection': 'required'}, # Create archive from selected files
288
+ 'extract_archive': ['U'], # Extract selected archive file
289
+
290
+ # === Search & Filter ===
291
+ 'search': ['F'], # Enter incremental search mode (isearch)
292
+ 'search_dialog': ['Shift-F'], # Show filename search dialog
293
+ 'search_content': ['Shift-G'], # Show content search dialog (grep)
294
+ 'filter': [';'], # Enter filter mode to show only matching files
295
+ 'clear_filter': [':'], # Clear current file filter
296
+
297
+ # === Sorting ===
298
+ 'sort_menu': ['S'], # Show sort options menu
299
+ 'quick_sort_name': ['1'], # Quick sort by filename
300
+ 'quick_sort_ext': ['2'], # Quick sort by file extension
301
+ 'quick_sort_size': ['3'], # Quick sort by file size
302
+ 'quick_sort_date': ['4'], # Quick sort by modification date
303
+
304
+ # === Directory Navigation ===
305
+ 'favorites': ['J'], # Show favorite directories dialog
306
+ 'jump_to_path': ['Shift-J'], # Jump to path
307
+ 'history': ['H'], # Show history for current pane
308
+ 'drives_dialog': ['D'], # Show drives/volumes dialog
309
+
310
+ # === Pane Management ===
311
+ 'sync_current_to_other': ['O'], # Sync current pane directory to other pane
312
+ 'sync_other_to_current': ['Shift-O'], # Sync other pane directory to current pane
313
+ 'compare_selection': ['W'], # Show file and directory comparison options
314
+ 'adjust_pane_left': ['['], # Make left pane smaller (move boundary left)
315
+ 'adjust_pane_right': [']'], # Make left pane larger (move boundary right)
316
+ 'reset_pane_boundary': ['-'], # Reset pane split to 50% | 50%
317
+
318
+ # === Log Pane Control ===
319
+ 'adjust_log_up': ['{'], # Make log pane larger (Shift+[)
320
+ 'adjust_log_down': ['}'], # Make log pane smaller (Shift+])
321
+ 'reset_log_height': ['_'], # Reset log pane height to default (Shift+-)
322
+ 'scroll_log_up': ['Shift-UP'], # Scroll log pane up one line
323
+ 'scroll_log_down': ['Shift-DOWN'], # Scroll log pane down one line
324
+ 'scroll_log_page_up': ['Shift-LEFT'], # Scroll log pane up one page (to older messages)
325
+ 'scroll_log_page_down': ['Shift-RIGHT'], # Scroll log pane down one page (to newer messages)
326
+
327
+ # === Text / Diff Viewer ===
328
+ # Viewer-only actions. 'search' (F, above) opens incremental search inside
329
+ # the viewers too. 'toggle_wrap' intentionally shares 'W' with
330
+ # 'compare_selection': they never apply in the same context (file list vs.
331
+ # open viewer), and each context matches its own action by name via
332
+ # KeyBindings.is_action_for_event, so the shared key is unambiguous.
333
+ 'toggle_wrap': ['W'], # Text viewer: toggle line wrapping
334
+ # 'M' likewise shares with 'move_files' / 'create_directory' (file list
335
+ # only); in an open viewer it toggles the raw text view and the file
336
+ # type's rich renderer (Markdown for *.md), matched by name in-context.
337
+ 'toggle_view_mode': ['M'], # Text viewer: toggle rendered (Markdown) / raw text
338
+
339
+ # === Image Viewer ===
340
+ # Image-viewer-only actions, matched by name in-context like the text
341
+ # viewer's above. '-' and '_' intentionally share with
342
+ # 'reset_pane_boundary' / 'reset_log_height': those apply to the file
343
+ # list only, never to an open viewer, and each context matches its own
344
+ # action by name via KeyBindings.is_action_for_event, so the shared keys
345
+ # are unambiguous. Arrow keys pan and Home/End jump to the first/last
346
+ # image; those stay viewer-local (not rebindable), like the text viewer's
347
+ # scroll keys.
348
+ 'image_zoom_in': ['+', '='], # Image viewer: zoom in ('=' is unshifted '+')
349
+ 'image_zoom_out': ['-', '_'], # Image viewer: zoom out
350
+ 'image_zoom_reset': ['0'], # Image viewer: fit the whole image to the window
351
+ 'image_next': ['n'], # Image viewer: next image in the file list
352
+ 'image_prev': ['p'], # Image viewer: previous image in the file list
353
+
354
+ # === Display & Appearance ===
355
+ 'toggle_hidden': ['.'], # Toggle visibility of hidden files (dotfiles)
356
+ 'toggle_color_scheme': ['T'], # Switch between dark and light color schemes
357
+ 'toggle_fallback_colors': ['Shift-T'], # Toggle fallback color mode for compatibility
358
+ 'view_options': ['Z'], # Show view options menu
359
+ 'settings_menu': ['Shift-Z'], # Show settings and configuration menu
360
+
361
+ # === External Programs ===
362
+ 'programs': ['X'], # Show external programs menu
363
+ 'subshell': ['Shift-X'], # Enter subshell (command line) mode
364
+
365
+ # === Configuration ===
366
+ # Unbound by default (reachable via the Tools menu). Assign a key here to
367
+ # open/reload this file without leaving XeFM, e.g. 'edit_config': ['Y'].
368
+ 'edit_config': [], # Edit this config.py in TEXT_EDITOR, then reload
369
+ 'reload_config': [], # Re-read this config.py and apply live
370
+ }
371
+
372
+ # Windows has no Command key, and Alt-Enter is the platform fullscreen-toggle
373
+ # convention — so the Mac-centric defaults above are unreachable there. Remap
374
+ # them to Ctrl equivalents on Windows.
375
+ if sys.platform == 'win32':
376
+ KEY_BINDINGS['open_with_os'] = ['Ctrl-ENTER'] # Open file(s) with OS default application
377
+ KEY_BINDINGS['reveal_in_os'] = ['Ctrl-Shift-E'] # Reveal focused file in Explorer
378
+ KEY_BINDINGS['copy_names'] = ['Ctrl-Shift-C'] # Copy selected/focused file name(s) to clipboard
379
+ KEY_BINDINGS['copy_paths'] = ['Ctrl-Shift-P'] # Copy selected/focused full path(s) to clipboard
380
+
381
+
382
+ # Favorite directories - customize your frequently used directories
383
+ # Each entry should have 'name' and 'path' keys
384
+ FAVORITE_DIRECTORIES = [
385
+ {'name': 'Home', 'path': '~'},
386
+ {'name': 'Documents', 'path': '~/Documents'},
387
+ {'name': 'Downloads', 'path': '~/Downloads'},
388
+ {'name': 'Desktop', 'path': '~/Desktop'},
389
+ {'name': 'Projects', 'path': '~/Projects'},
390
+ {'name': 'Root', 'path': '/'},
391
+ {'name': 'Temp', 'path': '/tmp'},
392
+ {'name': 'Config', 'path': '~/.config'},
393
+ # Add your own favorites here:
394
+ # {'name': 'Work', 'path': '/path/to/work'},
395
+ # {'name': 'Scripts', 'path': '~/bin'},
396
+ ]
397
+
398
+ # Performance settings
399
+ MAX_LOG_MESSAGES = 1000
400
+
401
+ # History settings
402
+ MAX_HISTORY_ENTRIES = 100 # Maximum number of history entries to keep
403
+
404
+ # Progress animation settings
405
+ PROGRESS_ANIMATION_PATTERN = 'spinner' # 'spinner', 'dots', 'progress', 'bounce', 'pulse', 'wave', 'clock', 'arrow'
406
+ PROGRESS_ANIMATION_SPEED = 0.2 # Animation frame update interval in seconds
407
+
408
+ # Motion settings
409
+ # Suppress decorative motion app-wide: dialogs appear at once instead of
410
+ # scaling in, and an animated theme background (Sci-Fi's starfield) coasts to
411
+ # a stop and holds a still frame. Everything lands in its FINAL state, never
412
+ # frozen mid-animation, so nothing is hidden by turning this on. Set it if
413
+ # motion is uncomfortable, or over a slow SSH link where every animated frame
414
+ # is a screen repaint. Functional updates (progress, file-list reloads,
415
+ # search results) are unaffected.
416
+ REDUCED_MOTION = False
417
+
418
+ # File display settings
419
+ SEPARATE_EXTENSIONS = True # Show file extensions separately from basenames
420
+ MAX_EXTENSION_LENGTH = 5 # Maximum extension length to show separately
421
+
422
+ # Text editor settings
423
+ # Supports both string and list formats:
424
+ # - String format: 'vim' (single command, no arguments)
425
+ # - List format: ['code', '--wait'] (command with arguments)
426
+ # Automatically set based on actual running backend mode:
427
+ # - Terminal mode (curses): vim
428
+ # - Desktop mode (coregraphics): code (VS Code)
429
+ TEXT_EDITOR = 'code' if is_desktop_mode() else 'vim'
430
+
431
+ # Text diff tool settings
432
+ # Tool invoked when pressing 'E' (edit_file) key in DiffViewer or DirectoryDiffViewer
433
+ # Supports both string and list formats:
434
+ # - String format: 'vimdiff' (single command, no arguments)
435
+ # - List format: ['code', '--diff'] (command with arguments)
436
+ # Automatically set based on actual running backend mode:
437
+ # - Terminal mode (curses): vimdiff (string format example)
438
+ # - Desktop mode (coregraphics): code --diff (list format example)
439
+ TEXT_DIFF = ['code', '--diff'] if is_desktop_mode() else 'vimdiff'
440
+
441
+ # S3 settings
442
+ S3_CACHE_TTL = 60 # S3 cache TTL in seconds (default: 60 seconds)
443
+
444
+ # SSH/SFTP cache settings
445
+ SSH_CACHE_TTL = 30 # SSH cache TTL in seconds for successful results (default: 30 seconds)
446
+ SSH_CACHE_ERROR_TTL = 300 # SSH cache TTL in seconds for cached errors (default: 300 seconds / 5 minutes)
447
+
448
+ # Archive cache settings
449
+ ARCHIVE_CACHE_MAX_OPEN = 5 # Maximum number of archives to keep open simultaneously
450
+ ARCHIVE_CACHE_TTL = 300 # Archive cache TTL in seconds (default: 300 seconds / 5 minutes)
451
+
452
+ # File monitoring settings
453
+ FILE_MONITORING_ENABLED = True # Enable/disable automatic file list reloading
454
+ FILE_MONITORING_COALESCE_DELAY_MS = 200 # Event coalescing window in milliseconds
455
+ FILE_MONITORING_MAX_RELOADS_PER_SECOND = 5 # Maximum reloads per second (rate limiting)
456
+ FILE_MONITORING_FALLBACK_POLL_INTERVAL_S = 5 # Polling interval for fallback mode (seconds)
457
+
458
+ # File extension associations
459
+ # Maps file patterns to what each action should do.
460
+ #
461
+ # There are two tiers of "open", and they are bound to different keys:
462
+ #
463
+ # 'enter' ENTER Casual open. Stays inside XeFM. The value names
464
+ # a built-in handler, NOT a program to launch:
465
+ # 'viewer' - the built-in text/markdown viewer
466
+ # 'navigate' - browse the file as an archive
467
+ # (handy for *.jar, *.whl, ...)
468
+ # None - do nothing
469
+ # With no rule, XeFM's default applies: directories
470
+ # and archives are entered, files open in the
471
+ # built-in viewer.
472
+ #
473
+ # 'open' Cmd/Ctrl-ENTER Deliberate open. Hands the file to an external
474
+ # program. Falls back to the OS default app.
475
+ #
476
+ # The other two actions are 'view' (V) and 'edit' (E), both external.
477
+ #
478
+ # Compact Format Features:
479
+ # 1. Multiple patterns in one entry: ['*.jpg', '*.jpeg', '*.png']
480
+ # 2. Combined actions: 'open|view' assigns same command to both actions
481
+ # 3. Commands: List ['open', '-a', 'Preview'] or string 'open -a Preview'
482
+ # 4. None: Action not available -- except 'view': None, which selects the
483
+ # built-in viewer, and 'enter': None, which does nothing.
484
+ #
485
+ # You do NOT declare whether a program takes over the terminal. That follows
486
+ # from the backend, not from the program: in terminal mode XeFM suspends and
487
+ # waits for the child (correct for less/vim; a launcher like `open -a` just
488
+ # returns straight away), and in desktop mode there is no terminal to hand
489
+ # over, so the child is detached and XeFM stays responsive. As with
490
+ # TEXT_EDITOR above, pick programs that suit the mode you run in.
491
+ #
492
+ # Format:
493
+ # {
494
+ # 'pattern': '*.pdf' or ['*.jpg', '*.png'], # Single or multiple fnmatch patterns
495
+ # 'enter': 'viewer', # Built-in handler for the ENTER key
496
+ # 'open|view': ['command'], # Same command for open and view
497
+ # 'edit': ['command'], # Different command for edit
498
+ # }
499
+ FILE_ASSOCIATIONS = [
500
+ # PDF files
501
+ {
502
+ 'pattern': '*.pdf',
503
+ 'open|view': ['open', '-a', 'Preview'],
504
+ 'edit': None,
505
+ },
506
+ # Image files. 'view' is deliberately None so V opens XeFM's own image
507
+ # viewer (zoom / pan / prev-next, staying inside XeFM) rather than handing
508
+ # the file to Preview; 'open' still hands it to the OS app for editing or
509
+ # a full-fidelity look. Set 'open|view' back to Preview if you would
510
+ # rather V left XeFM. Formats beyond these four still reach the built-in
511
+ # viewer by falling through with no rule at all.
512
+ {
513
+ 'pattern': ['*.jpg', '*.jpeg', '*.png', '*.gif'],
514
+ 'open': ['open', '-a', 'Preview'],
515
+ 'view': None,
516
+ 'edit': ['open', '-a', 'GIMP'],
517
+ },
518
+ # Video files
519
+ {
520
+ 'pattern': ['*.mp4', '*.mov'],
521
+ 'open|view': ['open', '-a', 'QuickTime Player'],
522
+ 'edit': None,
523
+ },
524
+ # Audio files
525
+ {
526
+ 'pattern': ['*.mp3', '*.wav'],
527
+ 'open': ['open', '-a', 'Music'],
528
+ 'edit': None,
529
+ },
530
+ # Microsoft Word documents
531
+ {
532
+ 'pattern': ['*.doc', '*.docx'],
533
+ 'open|view|edit': ['open', '-a', 'Microsoft Word'],
534
+ },
535
+ # Microsoft Excel spreadsheets
536
+ {
537
+ 'pattern': ['*.xls', '*.xlsx'],
538
+ 'open|view|edit': ['open', '-a', 'Microsoft Excel'],
539
+ },
540
+ # Microsoft PowerPoint presentations
541
+ {
542
+ 'pattern': ['*.ppt', '*.pptx'],
543
+ 'open|view|edit': ['open', '-a', 'Microsoft PowerPoint'],
544
+ },
545
+ # Note there is deliberately no entry listing text/code extensions.
546
+ # Enter and V already fall through to the built-in viewer when no rule
547
+ # matches, and the viewer sniffs the bytes -- so it reads text with no
548
+ # configuration and shows a placeholder for binaries, including for
549
+ # files with no extension or an unknown one that a list would miss.
550
+ #
551
+ # Zip-shaped archives XeFM does not enter by extension. This one *is*
552
+ # worth stating, because it cannot be sniffed: .docx and .xlsx are zip
553
+ # files too, and you want Word for those, not a file listing.
554
+ {
555
+ 'pattern': ['*.jar', '*.whl', '*.egg'],
556
+ 'enter': 'navigate',
557
+ },
558
+ # Add your own file associations here:
559
+ # {
560
+ # 'pattern': ['*.ext1', '*.ext2'],
561
+ # 'enter': 'viewer', # what ENTER does (in XeFM)
562
+ # 'open|view': ['command', 'args'], # external programs
563
+ # 'edit': ['command', 'args'],
564
+ # },
565
+ # Terminal programs need no special marking -- in terminal mode XeFM
566
+ # hands the display over and waits:
567
+ # {
568
+ # 'pattern': '*.log',
569
+ # 'view': ['less'],
570
+ # },
571
+ ]
572
+
573
+ # External programs - each item has "name", "command", and optional "options" fields
574
+ # The "command" field is a list for safe subprocess execution
575
+ # Relative paths in the first element are resolved relative to the XeFM root directory (where xefm/app.py is located)
576
+ # Use xefm_tool('tool_name') to search for tools in:
577
+ # 1. ~/.xefm/tools/ (user-specific tools, highest priority)
578
+ # 2. {xefm/app.py directory}/tools/ (system tools, fallback)
579
+ # The "options" field is a dictionary with program-specific options:
580
+ # - auto_return: if True, automatically returns to XeFM without waiting for user input
581
+ PROGRAMS = [
582
+ {'name': 'Compare Files (BeyondCompare)', 'command': [xefm_python, xefm_tool('bcompare_files.py')], 'options': {'auto_return': True}},
583
+ {'name': 'Compare Directories (BeyondCompare)', 'command': [xefm_python, xefm_tool('bcompare_dirs.py')], 'options': {'auto_return': True}},
584
+ {'name': 'Open in VSCode', 'command': [xefm_python, xefm_tool('vscode.py')], 'options': {'auto_return': True}},
585
+ {'name': 'Open in Kiro', 'command': [xefm_python, xefm_tool('kiro.py')], 'options': {'auto_return': True}},
586
+
587
+ # Add your own programs here:
588
+ # {'name': 'My Custom Tool', 'command': [xefm_python, xefm_tool('my_custom_tool.py')], 'options': {'auto_return': True}},
589
+ # {'name': 'My Script (direct path)', 'command': [xefm_python, '/path/to/script.py']},
590
+ # {'name': 'Python REPL', 'command': ['python3']},
591
+ # {'name': 'Quick Command', 'command': ['ls', '-la'], 'options': {'auto_return': True}},
592
+ ]