rtfm-filemanager 8.1.3 → 8.2.1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +22 -0
- data/README.md +87 -38
- data/bin/rtfm +275 -10
- data/examples/bookmarks.rb +137 -0
- data/examples/diskusage.rb +199 -0
- data/examples/dupes.rb +335 -0
- data/examples/git.rb +73 -0
- data/examples/notes.rb +92 -0
- data/examples/opener.rb +87 -0
- data/examples/settings.rb +186 -0
- metadata +13 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: eb02bbf2f12ccc66afd6d2fbbef68f6e7a1a05f3d932e48a8143ae8df1a70d39
|
|
4
|
+
data.tar.gz: e1a1583ccab118308ecf16d898631dff1fcddedb3b46f7462ba2a8486cfad5d1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8eaeb2806d03dd15601d705bc409b677cbbb3bbe08aa69a1cd8b9135bbf4b5ecfc95c8140b4c9d9e526eb4652fa9057f4102aec05058d6c94272b39027d3e33a
|
|
7
|
+
data.tar.gz: f9cb7747b1c773a2c545bb1afaa600ceff1a04d13149d9a98cae029687a37cbeccb0b330951659463fd666206104234d5ae0f6298c5cef2ff01d3950be9063ae
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,28 @@ All notable changes to RTFM will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [8.2.1] - 2026-03-16
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Trash browser** (`D` key) - Browse trash contents with timestamps and original paths. Restore files, permanently delete individual items, or empty all trash
|
|
12
|
+
- **Disk usage plugin** (`S` key) - Interactive size-sorted directory analyzer with visual bars and cancellable scanning
|
|
13
|
+
- **Duplicate finder plugin** (`F7`) - Find duplicate files by content hash with tag-to-keep workflow and cancellable scanning
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- **Right pane scrolling** - Shift-Down/Up now works after displaying content via `say` (marks, help, system info, etc.)
|
|
17
|
+
- **rcurses pane refresh** - No longer queries cursor position via stdin (uses ANSI save/restore), preventing keypresses from being consumed during progress updates
|
|
18
|
+
|
|
19
|
+
## [8.2.0] - 2026-03-15
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- **Plugin system** - Plugins in `~/.rtfm/plugins/` are auto-loaded on startup. Each plugin is a `.rb` file with metadata comments (`@name`, `@description`, `@key`). Disabled plugins use `.rb.off` extension
|
|
23
|
+
- **Plugin manager** (`V` key) - Enable/disable plugins live without restarting RTFM. Press `?` on any plugin to view its built-in help text
|
|
24
|
+
- **Settings plugin** (`C` key) - Interactive editor for settings without dedicated keys: trash mode, run-mailcap, interactive programs, ls flags, OpenAI config, and all pane colors with live preview
|
|
25
|
+
- **Git plugin** (`Ctrl-G`) - Quick git menu: status, diff, commit+push, and log, all from within RTFM
|
|
26
|
+
- **Bookmarks plugin** (`F6`) - Unlimited directory bookmarks with fuzzy filtering, complementing the built-in single-letter marks
|
|
27
|
+
- **Notes plugin** (`F5`) - Attach text notes to any file or directory, stored in `~/.rtfm/notes/`
|
|
28
|
+
- **Opener plugin** (`RIGHT`/`l`) - Define custom programs for specific file extensions via a simple hash
|
|
29
|
+
|
|
8
30
|
## [8.1.3] - 2026-03-13
|
|
9
31
|
|
|
10
32
|
### Fixed
|
data/README.md
CHANGED
|
@@ -92,10 +92,10 @@ After first run, use `r` command to launch RTFM and exit into your current direc
|
|
|
92
92
|
- **Navi integration** - Interactive command cheatsheets
|
|
93
93
|
|
|
94
94
|
### Developer Features
|
|
95
|
-
- **Plugin
|
|
95
|
+
- **Plugin system** - Live enable/disable plugins with built-in manager (`V` key)
|
|
96
|
+
- **Example plugins** - Settings editor, git, bookmarks, notes, custom openers
|
|
96
97
|
- **Ruby debug mode** - Execute arbitrary Ruby in context
|
|
97
98
|
- **Command history** - Preserved across sessions
|
|
98
|
-
- **Extensible** - Clean plugin API
|
|
99
99
|
|
|
100
100
|
---
|
|
101
101
|
|
|
@@ -168,6 +168,7 @@ For complete reference: `man rtfm` or press `?` in RTFM
|
|
|
168
168
|
| `q` | Quit (save config) |
|
|
169
169
|
| `Q` | Quit (don't save) |
|
|
170
170
|
| `r` | Refresh display |
|
|
171
|
+
| `V` | Plugin manager |
|
|
171
172
|
|
|
172
173
|
### Navigation
|
|
173
174
|
|
|
@@ -210,7 +211,7 @@ For complete reference: `man rtfm` or press `?` in RTFM
|
|
|
210
211
|
| `c` | Rename item |
|
|
211
212
|
| `E` | Bulk rename (patterns) |
|
|
212
213
|
| `d` | Delete (→ trash if enabled) |
|
|
213
|
-
| `D` |
|
|
214
|
+
| `D` | Trash browser (browse, restore, delete) |
|
|
214
215
|
| `Ctrl-d` | Toggle trash on/off |
|
|
215
216
|
| `U` | Undo last operation |
|
|
216
217
|
|
|
@@ -359,10 +360,12 @@ Configuration stored in `~/.rtfm/conf`
|
|
|
359
360
|
|
|
360
361
|
| Action | Command |
|
|
361
362
|
|--------|---------|
|
|
362
|
-
| View config | Press `C` |
|
|
363
|
+
| View config | Press `C` (or interactive editor with Settings plugin) |
|
|
363
364
|
| Save config | Press `W` |
|
|
364
365
|
| Reload config | Press `R` |
|
|
365
366
|
|
|
367
|
+
**Tip:** Install the Settings plugin for an interactive settings editor with live color preview. See [Plugins](#plugins).
|
|
368
|
+
|
|
366
369
|
### Common Settings
|
|
367
370
|
|
|
368
371
|
```ruby
|
|
@@ -492,50 +495,79 @@ The chat maintains context throughout your RTFM session, so follow-up questions
|
|
|
492
495
|
|
|
493
496
|
## Plugins
|
|
494
497
|
|
|
495
|
-
RTFM
|
|
498
|
+
RTFM has a plugin system with live enable/disable. Plugins live in `~/.rtfm/plugins/` as `.rb` files and are auto-loaded on startup.
|
|
496
499
|
|
|
497
|
-
###
|
|
500
|
+
### Plugin Manager (`V` key)
|
|
498
501
|
|
|
499
|
-
|
|
502
|
+
Press `V` to open the built-in plugin manager:
|
|
503
|
+
- See all available plugins with their status (ON/OFF)
|
|
504
|
+
- Press `ENTER` to toggle a plugin on or off (takes effect immediately)
|
|
505
|
+
- Press `?` to view the plugin's built-in help text
|
|
506
|
+
- No restart required
|
|
500
507
|
|
|
501
|
-
|
|
502
|
-
# ~/.rtfm/plugins/preview.rb
|
|
508
|
+
### Example Plugins
|
|
503
509
|
|
|
504
|
-
|
|
505
|
-
# @s is replaced with shell-escaped filename
|
|
510
|
+
RTFM ships with five example plugins in the `examples/` directory. Copy any of them to `~/.rtfm/plugins/` to activate:
|
|
506
511
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
+
```bash
|
|
513
|
+
cp examples/settings.rb ~/.rtfm/plugins/
|
|
514
|
+
cp examples/git.rb ~/.rtfm/plugins/
|
|
515
|
+
cp examples/bookmarks.rb ~/.rtfm/plugins/
|
|
516
|
+
cp examples/notes.rb ~/.rtfm/plugins/
|
|
517
|
+
cp examples/opener.rb ~/.rtfm/plugins/
|
|
518
|
+
cp examples/diskusage.rb ~/.rtfm/plugins/
|
|
519
|
+
cp examples/dupes.rb ~/.rtfm/plugins/
|
|
512
520
|
```
|
|
513
521
|
|
|
514
|
-
|
|
522
|
+
| Plugin | Key | Description |
|
|
523
|
+
|--------|-----|-------------|
|
|
524
|
+
| **Settings** | `C` | Interactive editor for settings without dedicated keys (colors, trash, interactive programs, OpenAI config). Color changes apply in real-time. Overrides the default config viewer |
|
|
525
|
+
| **Git** | `Ctrl-G` | Git operations menu: status, diff, commit+push, log. Output shown in right pane |
|
|
526
|
+
| **Bookmarks** | `F6` | Unlimited directory bookmarks with fuzzy filtering. Add, delete, and jump to bookmarked directories. Complements the built-in single-letter marks (m/') |
|
|
527
|
+
| **Notes** | `F5` | Attach text notes to any file or directory. View, edit, or delete notes. Stored in `~/.rtfm/notes/` |
|
|
528
|
+
| **Opener** | `RIGHT`/`l` | Custom file openers by extension. Configure a hash of extension-to-command mappings (e.g., `.hl` files open in hyperlist) |
|
|
529
|
+
| **Disk Usage** | `S` | Interactive disk usage analyzer. Extends system info with a browseable size-sorted directory view with visual bars |
|
|
530
|
+
| **Dupes** | `F7` | Find duplicate files by content hash. Two-pass scan (size then SHA256) with in-place deletion of duplicates |
|
|
531
|
+
|
|
532
|
+
### Writing Your Own Plugins
|
|
515
533
|
|
|
516
|
-
|
|
534
|
+
A plugin is a simple Ruby file with metadata comments and KEYMAP bindings:
|
|
517
535
|
|
|
518
536
|
```ruby
|
|
519
|
-
#
|
|
537
|
+
# @name: My Plugin
|
|
538
|
+
# @description: What it does
|
|
539
|
+
# @key: X
|
|
540
|
+
|
|
541
|
+
KEYMAP['X'] = :my_action
|
|
542
|
+
|
|
543
|
+
# Optional: register help text shown by ? in plugin manager
|
|
544
|
+
PLUGIN_HELP['My Plugin'] = <<~HELP
|
|
545
|
+
This is the help text for my plugin.
|
|
546
|
+
Shown when pressing ? in the plugin manager.
|
|
547
|
+
HELP
|
|
548
|
+
|
|
549
|
+
def my_action
|
|
550
|
+
clear_image
|
|
551
|
+
@pR.update = true
|
|
552
|
+
@pR.say("Hello from my plugin!")
|
|
553
|
+
end
|
|
554
|
+
```
|
|
520
555
|
|
|
521
|
-
|
|
522
|
-
KEYMAP['Z'] = :my_custom_action
|
|
556
|
+
Plugins can also be defined in `~/.rtfm/keys.rb` for personal bindings that don't need the enable/disable mechanism.
|
|
523
557
|
|
|
524
|
-
|
|
525
|
-
@pB.say("Custom action triggered!")
|
|
526
|
-
# Use @pL, @pR, @selected, etc.
|
|
527
|
-
end
|
|
558
|
+
### Preview Handlers (`preview.rb`)
|
|
528
559
|
|
|
529
|
-
|
|
530
|
-
KEYMAP['C-G'] = :git_commit
|
|
560
|
+
Custom file type previews are configured separately in `~/.rtfm/preview.rb`:
|
|
531
561
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
562
|
+
```ruby
|
|
563
|
+
# Syntax: ext1, ext2 = command with @s placeholder
|
|
564
|
+
txt, log = bat -n --color=always @s
|
|
565
|
+
md = pandoc @s -t plain
|
|
566
|
+
pdf = pdftotext -f 1 -l 4 @s -
|
|
567
|
+
json = jq . @s
|
|
536
568
|
```
|
|
537
569
|
|
|
538
|
-
### Available Variables
|
|
570
|
+
### Available Variables for Plugin Authors
|
|
539
571
|
|
|
540
572
|
| Variable | Description |
|
|
541
573
|
|----------|-------------|
|
|
@@ -543,22 +575,33 @@ end
|
|
|
543
575
|
| `@pL` | Left pane (file list) |
|
|
544
576
|
| `@pR` | Right pane (preview) |
|
|
545
577
|
| `@pB` | Bottom pane (status) |
|
|
546
|
-
| `@pCmd` | Command prompt |
|
|
578
|
+
| `@pCmd` | Command prompt (use `.ask(prompt, default)` for input) |
|
|
547
579
|
| `@selected` | Currently selected file/dir |
|
|
548
580
|
| `@tagged` | Array of tagged items |
|
|
549
581
|
| `@external_program_running` | Set true when launching TUI programs |
|
|
582
|
+
| `PLUGIN_HELP` | Hash to register help text (keyed by plugin name) |
|
|
550
583
|
|
|
551
584
|
### Plugin Helper Functions
|
|
552
585
|
|
|
553
586
|
```ruby
|
|
554
|
-
# Capture command output
|
|
587
|
+
# Capture command output as string
|
|
555
588
|
output = command("ls -la", timeout: 5)
|
|
556
589
|
|
|
557
|
-
# Run command
|
|
558
|
-
shell("mv file1 file2", background: false)
|
|
559
|
-
|
|
560
|
-
# Run and show both stdout/stderr in right pane
|
|
590
|
+
# Run command interactively (full terminal)
|
|
561
591
|
shellexec("grep -r pattern .")
|
|
592
|
+
|
|
593
|
+
# Read a keypress
|
|
594
|
+
chr = getchr
|
|
595
|
+
|
|
596
|
+
# Clear any displayed image
|
|
597
|
+
clear_image
|
|
598
|
+
|
|
599
|
+
# Text formatting
|
|
600
|
+
"text".fg(112) # foreground color (0-255)
|
|
601
|
+
"text".bg(236) # background color
|
|
602
|
+
"text".b # bold
|
|
603
|
+
"text".u # underline
|
|
604
|
+
"text".r # reverse
|
|
562
605
|
```
|
|
563
606
|
|
|
564
607
|
---
|
|
@@ -650,6 +693,12 @@ Best image experience with: kitty, urxvt, xterm, mlterm, foot
|
|
|
650
693
|
|
|
651
694
|
## Latest Updates
|
|
652
695
|
|
|
696
|
+
### Version 8.2 Highlights
|
|
697
|
+
|
|
698
|
+
- **Plugin system with live enable/disable** - Plugins in `~/.rtfm/plugins/` are auto-loaded on startup. Toggle them on and off at runtime with the plugin manager (`V` key), no restart needed. Each plugin can register help text viewable with `?`.
|
|
699
|
+
- **Seven example plugins** - Settings editor, git operations, directory bookmarks, file notes, custom file openers, disk usage analyzer, and duplicate file finder. Copy from `examples/` to `~/.rtfm/plugins/` to use.
|
|
700
|
+
- **Trash browser** (`D` key) - Browse trash contents with timestamps and original paths. Restore files to their original location, permanently delete individual items, or empty all trash.
|
|
701
|
+
|
|
653
702
|
### Version 8.1 Highlights
|
|
654
703
|
|
|
655
704
|
- **File picker mode** - `rtfm --pick=/path/to/output.txt` launches RTFM as a file selector. Browse and tag files normally with `t`, then quit with `q`. Tagged file paths are written one per line to the output file. Enables integration with email clients, upload tools, and other applications that need a file selection dialog.
|
data/bin/rtfm
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
# get a great understanding of the code itself by simply sending
|
|
19
19
|
# or pasting this whole file into you favorite AI for coding with
|
|
20
20
|
# a prompt like this: "Help me understand every part of this code".
|
|
21
|
-
@version = '8.1
|
|
21
|
+
@version = '8.2.1' # Trash browser, right pane scroll fix, plugin improvements
|
|
22
22
|
|
|
23
23
|
# SAVE & STORE TERMINAL {{{1
|
|
24
24
|
ORIG_STTY = `stty -g`.chomp
|
|
@@ -173,6 +173,7 @@ CONFIG_FILE = File.join(RTFM_HOME, 'conf')
|
|
|
173
173
|
W = Write parameters to ~/.rtfm/conf: @marks, @hash, @history, @rubyhistory, @aihistory, @sshhistory
|
|
174
174
|
@lslong, @lsall, @lsorder, @lsinvert, @width, @border, @preview, @trash
|
|
175
175
|
C = Show the current configuration in ~/.rtfm/conf
|
|
176
|
+
V = Plugin manager (enable/disable plugins from ~/.rtfm/plugins/)
|
|
176
177
|
q = Quit (save basic configuration: @marks, @hash, @history, @rubyhistory, @aihistory, @sshhistory)
|
|
177
178
|
Q = QUIT (without writing any changes to the config file)
|
|
178
179
|
|
|
@@ -241,7 +242,7 @@ CONFIG_FILE = File.join(RTFM_HOME, 'conf')
|
|
|
241
242
|
s = Create symlink to tagged items here
|
|
242
243
|
d = Delete selected item and tagged items. Confirm with 'y'.
|
|
243
244
|
Moves items to trash directory (~/.rtfm/trash/) if @trash = true
|
|
244
|
-
D =
|
|
245
|
+
D = Trash browser (browse, restore, delete, empty trash)
|
|
245
246
|
Ctrl-d = Toggle use of trash directory
|
|
246
247
|
|
|
247
248
|
UNDO OPERATIONS
|
|
@@ -928,6 +929,7 @@ KEYMAP = { # {{{2
|
|
|
928
929
|
'R' => :load_config,
|
|
929
930
|
'C' => :show_config,
|
|
930
931
|
'W' => :write_config,
|
|
932
|
+
'V' => :plugin_manager,
|
|
931
933
|
'q' => :quit_and_save,
|
|
932
934
|
'Q' => :quit_no_save,
|
|
933
935
|
|
|
@@ -1078,6 +1080,78 @@ if File.exist?(KEYS_FILE)
|
|
|
1078
1080
|
end
|
|
1079
1081
|
end
|
|
1080
1082
|
|
|
1083
|
+
# PLUGIN SYSTEM {{{2
|
|
1084
|
+
# Plugins live in ~/.rtfm/plugins/ as .rb files.
|
|
1085
|
+
# Each plugin should start with metadata comments:
|
|
1086
|
+
# # @name: My Plugin
|
|
1087
|
+
# # @description: What it does
|
|
1088
|
+
# # @key: X (optional, documents which key it binds)
|
|
1089
|
+
# Disabled plugins have .rb.off extension.
|
|
1090
|
+
|
|
1091
|
+
@plugins = {} # name => { file:, enabled:, meta:, saved_keys: }
|
|
1092
|
+
PLUGIN_HELP = {} # name => help string (registered by each plugin)
|
|
1093
|
+
|
|
1094
|
+
def parse_plugin_meta(file)
|
|
1095
|
+
meta = { name: File.basename(file).sub(/\.rb(\.off)?$/, ''), description: '', key: '' }
|
|
1096
|
+
File.foreach(file) do |line|
|
|
1097
|
+
break unless line.start_with?('#')
|
|
1098
|
+
meta[:name] = $1.strip if line =~ /^#\s*@name:\s*(.+)/
|
|
1099
|
+
meta[:description] = $1.strip if line =~ /^#\s*@description:\s*(.+)/
|
|
1100
|
+
meta[:key] = $1.strip if line =~ /^#\s*@key:\s*(.+)/
|
|
1101
|
+
end
|
|
1102
|
+
meta
|
|
1103
|
+
end
|
|
1104
|
+
|
|
1105
|
+
def load_plugin(name)
|
|
1106
|
+
p = @plugins[name]
|
|
1107
|
+
return unless p
|
|
1108
|
+
snapshot = KEYMAP.dup
|
|
1109
|
+
begin
|
|
1110
|
+
load p[:file].sub(/\.off$/, '')
|
|
1111
|
+
# Track which KEYMAP entries changed
|
|
1112
|
+
changed = {}
|
|
1113
|
+
KEYMAP.each { |k, v| changed[k] = snapshot[k] if snapshot[k] != v }
|
|
1114
|
+
snapshot.each { |k, v| changed[k] = v unless KEYMAP.key?(k) }
|
|
1115
|
+
p[:saved_keys] = changed
|
|
1116
|
+
p[:enabled] = true
|
|
1117
|
+
rescue => e
|
|
1118
|
+
@plugin_errors << "Error loading plugin #{name}: #{e.class}: #{e.message}"
|
|
1119
|
+
end
|
|
1120
|
+
end
|
|
1121
|
+
|
|
1122
|
+
def unload_plugin(name)
|
|
1123
|
+
p = @plugins[name]
|
|
1124
|
+
return unless p
|
|
1125
|
+
(p[:saved_keys] || {}).each do |k, original|
|
|
1126
|
+
original ? KEYMAP[k] = original : KEYMAP.delete(k)
|
|
1127
|
+
end
|
|
1128
|
+
p[:enabled] = false
|
|
1129
|
+
end
|
|
1130
|
+
|
|
1131
|
+
def toggle_plugin(name)
|
|
1132
|
+
p = @plugins[name]
|
|
1133
|
+
return unless p
|
|
1134
|
+
base = p[:file].sub(/\.off$/, '')
|
|
1135
|
+
off = base + '.off'
|
|
1136
|
+
if p[:enabled]
|
|
1137
|
+
unload_plugin(name)
|
|
1138
|
+
File.rename(base, off) if File.exist?(base)
|
|
1139
|
+
p[:file] = off
|
|
1140
|
+
else
|
|
1141
|
+
File.rename(off, base) if File.exist?(off)
|
|
1142
|
+
p[:file] = base
|
|
1143
|
+
load_plugin(name)
|
|
1144
|
+
end
|
|
1145
|
+
end
|
|
1146
|
+
|
|
1147
|
+
# Scan and load enabled plugins
|
|
1148
|
+
Dir.glob(File.join(PLUGINS_DIR, '*.rb{,.off}')).sort.each do |f|
|
|
1149
|
+
meta = parse_plugin_meta(f)
|
|
1150
|
+
enabled = !f.end_with?('.off')
|
|
1151
|
+
@plugins[meta[:name]] = { file: f, enabled: false, meta: meta, saved_keys: {} }
|
|
1152
|
+
load_plugin(meta[:name]) if enabled
|
|
1153
|
+
end
|
|
1154
|
+
|
|
1081
1155
|
# MAIN GETKEY FOR USER INPUT {{{2
|
|
1082
1156
|
def getkey # {{{3
|
|
1083
1157
|
chr = getchr(1)
|
|
@@ -1197,6 +1271,59 @@ def show_config
|
|
|
1197
1271
|
@pR.say('Configuration'.u.fg(254) + ":\n\n" + @conf.fg(249))
|
|
1198
1272
|
end
|
|
1199
1273
|
|
|
1274
|
+
def plugin_manager # {{{3
|
|
1275
|
+
clear_image
|
|
1276
|
+
names = @plugins.keys.sort
|
|
1277
|
+
if names.empty?
|
|
1278
|
+
@pR.say("Plugins".u.fg(254) + "\n\nNo plugins found in ~/.rtfm/plugins/\n\nPlace .rb files there to make them available.")
|
|
1279
|
+
return
|
|
1280
|
+
end
|
|
1281
|
+
sel = 0
|
|
1282
|
+
loop do
|
|
1283
|
+
lines = []
|
|
1284
|
+
lines << "Plugins".b.fg(254)
|
|
1285
|
+
lines << ""
|
|
1286
|
+
names.each_with_index do |name, i|
|
|
1287
|
+
p = @plugins[name]
|
|
1288
|
+
status = p[:enabled] ? " ON ".bg(22).fg(255) : " OFF".bg(52).fg(255)
|
|
1289
|
+
key_info = p[:meta][:key].empty? ? '' : " [#{p[:meta][:key]}]".fg(240)
|
|
1290
|
+
desc = p[:meta][:description].empty? ? '' : " #{p[:meta][:description]}".fg(249)
|
|
1291
|
+
line = "#{status} #{name}#{key_info}#{desc}"
|
|
1292
|
+
line = i == sel ? line.u : line
|
|
1293
|
+
lines << line
|
|
1294
|
+
end
|
|
1295
|
+
lines << ""
|
|
1296
|
+
lines << "j/k:move ENTER:toggle ?:help q:close".fg(240)
|
|
1297
|
+
@pR.update = true
|
|
1298
|
+
@pR.say(lines.join("\n"))
|
|
1299
|
+
|
|
1300
|
+
chr = getchr
|
|
1301
|
+
case chr
|
|
1302
|
+
when 'q', 'ESC'
|
|
1303
|
+
break
|
|
1304
|
+
when 'j', 'DOWN'
|
|
1305
|
+
sel = (sel + 1) % names.size
|
|
1306
|
+
when 'k', 'UP'
|
|
1307
|
+
sel = (sel - 1) % names.size
|
|
1308
|
+
when 'ENTER', 'RIGHT', 'LEFT'
|
|
1309
|
+
toggle_plugin(names[sel])
|
|
1310
|
+
when '?'
|
|
1311
|
+
name = names[sel]
|
|
1312
|
+
help = PLUGIN_HELP[name]
|
|
1313
|
+
if help
|
|
1314
|
+
@pR.update = true
|
|
1315
|
+
@pR.say(name.b.fg(254) + " help\n\n" + help)
|
|
1316
|
+
getchr
|
|
1317
|
+
else
|
|
1318
|
+
@pB.say("No help available for #{name}.")
|
|
1319
|
+
end
|
|
1320
|
+
end
|
|
1321
|
+
end
|
|
1322
|
+
@pR.update = true
|
|
1323
|
+
refresh
|
|
1324
|
+
render
|
|
1325
|
+
end
|
|
1326
|
+
|
|
1200
1327
|
def write_config # {{{3
|
|
1201
1328
|
conf_write(all: true)
|
|
1202
1329
|
show_config
|
|
@@ -4077,14 +4204,152 @@ def empty_trash # {{{3
|
|
|
4077
4204
|
remote_download_selected
|
|
4078
4205
|
return
|
|
4079
4206
|
end
|
|
4080
|
-
|
|
4081
|
-
|
|
4207
|
+
trash_browser
|
|
4208
|
+
end
|
|
4209
|
+
|
|
4210
|
+
def trash_browser # TRASH BROWSER {{{3
|
|
4211
|
+
clear_image
|
|
4212
|
+
sel = 0
|
|
4213
|
+
offset = 0
|
|
4214
|
+
|
|
4215
|
+
loop do
|
|
4216
|
+
# Scan trash directory, build item list with metadata
|
|
4217
|
+
raw = Dir.entries(TRASH_DIR).reject { |e| e == '.' || e == '..' }.map { |e| File.join(TRASH_DIR, e) }
|
|
4218
|
+
entries = raw.select { |f| File.exist?(f) || File.symlink?(f) }
|
|
4219
|
+
entries.sort_by! { |f| File.mtime(f) rescue Time.at(0) }
|
|
4220
|
+
entries.reverse!
|
|
4221
|
+
items = entries.filter_map do |f|
|
|
4222
|
+
basename = File.basename(f)
|
|
4223
|
+
orig_name = basename.sub(/^\d{8}_\d{6}_/, '').sub(/_\d{4}$/, '')
|
|
4224
|
+
orig_path = nil
|
|
4225
|
+
@undo_history.each do |op|
|
|
4226
|
+
next unless op[:type] == 'delete' && op[:trash]
|
|
4227
|
+
op[:paths].each { |p| orig_path = p[:path] if p[:trash_name] == basename }
|
|
4228
|
+
end
|
|
4229
|
+
begin
|
|
4230
|
+
size = File.directory?(f) ? (command("du -sb #{Shellwords.escape(f)} 2>/dev/null").split.first.to_i rescue 0) : File.size(f)
|
|
4231
|
+
mtime = File.mtime(f)
|
|
4232
|
+
rescue
|
|
4233
|
+
size = 0
|
|
4234
|
+
mtime = Time.at(0)
|
|
4235
|
+
end
|
|
4236
|
+
{ path: f, basename: basename, orig_name: orig_name, orig_path: orig_path,
|
|
4237
|
+
size: size, mtime: mtime, dir: File.directory?(f) }
|
|
4238
|
+
end
|
|
4239
|
+
|
|
4240
|
+
sel = [sel, items.size - 1, 0].sort[1]
|
|
4241
|
+
page_h = @pR.h - 5
|
|
4242
|
+
page_h = 1 if page_h < 1
|
|
4243
|
+
offset = sel - page_h + 1 if sel >= offset + page_h
|
|
4244
|
+
offset = sel if sel < offset
|
|
4245
|
+
offset = 0 if offset < 0
|
|
4246
|
+
visible = items[offset, page_h] || []
|
|
4247
|
+
|
|
4248
|
+
lines = []
|
|
4249
|
+
lines << "j/k:move r:restore d:delete E:empty all q:close".fg(240)
|
|
4250
|
+
lines << "Trash".b.fg(254) + " (#{items.size} items)".fg(240)
|
|
4251
|
+
lines << ""
|
|
4252
|
+
|
|
4253
|
+
if items.empty?
|
|
4254
|
+
lines << "Trash is empty.".fg(240)
|
|
4255
|
+
else
|
|
4256
|
+
visible.each_with_index do |item, i|
|
|
4257
|
+
idx = offset + i
|
|
4258
|
+
size_s = human_size(item[:size])
|
|
4259
|
+
type_indicator = item[:dir] ? "/".fg(69) : ""
|
|
4260
|
+
age = time_ago(item[:mtime])
|
|
4261
|
+
name = item[:orig_name] + type_indicator
|
|
4262
|
+
origin = item[:orig_path] ? " #{File.dirname(item[:orig_path])}".fg(240) : ""
|
|
4263
|
+
line = "#{size_s.rjust(8).fg(249)} #{age.rjust(6).fg(240)} #{name}#{origin}"
|
|
4264
|
+
line = idx == sel ? line.u : line
|
|
4265
|
+
lines << line
|
|
4266
|
+
end
|
|
4267
|
+
lines << "(#{sel + 1}/#{items.size})".fg(240) if items.size > page_h
|
|
4268
|
+
end
|
|
4269
|
+
|
|
4270
|
+
@pR.update = true
|
|
4271
|
+
@pR.say(lines.join("\n"))
|
|
4272
|
+
|
|
4273
|
+
chr = getchr
|
|
4274
|
+
case chr
|
|
4275
|
+
when 'q', 'ESC'
|
|
4276
|
+
break
|
|
4277
|
+
when 'j', 'DOWN'
|
|
4278
|
+
sel = items.empty? ? 0 : (sel + 1) % items.size
|
|
4279
|
+
when 'k', 'UP'
|
|
4280
|
+
sel = items.empty? ? 0 : (sel - 1) % items.size
|
|
4281
|
+
when 'r'
|
|
4282
|
+
next if items.empty?
|
|
4283
|
+
item = items[sel]
|
|
4284
|
+
if item[:orig_path]
|
|
4285
|
+
dest = item[:orig_path]
|
|
4286
|
+
else
|
|
4287
|
+
dest = @pCmd.ask('Restore to: ', File.join(Dir.pwd, item[:orig_name]))
|
|
4288
|
+
end
|
|
4289
|
+
next if dest.strip.empty?
|
|
4290
|
+
if File.exist?(dest)
|
|
4291
|
+
@pB.say("Destination exists: #{dest}".fg(196))
|
|
4292
|
+
next
|
|
4293
|
+
end
|
|
4294
|
+
begin
|
|
4295
|
+
FileUtils.mv(item[:path], dest)
|
|
4296
|
+
# Remove from undo history
|
|
4297
|
+
@undo_history.each do |op|
|
|
4298
|
+
next unless op[:type] == 'delete' && op[:trash]
|
|
4299
|
+
op[:paths].reject! { |p| p[:trash_name] == item[:basename] }
|
|
4300
|
+
end
|
|
4301
|
+
@pB.say("Restored: #{File.basename(dest)}".fg(156))
|
|
4302
|
+
sel = [sel - 1, 0].max
|
|
4303
|
+
rescue => e
|
|
4304
|
+
@pB.say("Restore failed: #{e.message}".fg(196))
|
|
4305
|
+
end
|
|
4306
|
+
when 'd'
|
|
4307
|
+
next if items.empty?
|
|
4308
|
+
item = items[sel]
|
|
4309
|
+
@pB.say(" Permanently delete #{item[:orig_name]}? (y/n)".fg(196))
|
|
4310
|
+
if getchr == 'y'
|
|
4311
|
+
command("rm -rf #{Shellwords.escape(item[:path])}")
|
|
4312
|
+
@pB.say("Deleted: #{item[:orig_name]}".fg(204))
|
|
4313
|
+
sel = [sel - 1, 0].max
|
|
4314
|
+
end
|
|
4315
|
+
when 'E'
|
|
4316
|
+
@pB.say(" Empty ALL trash? This cannot be undone! (y/n)".fg(196))
|
|
4317
|
+
if getchr == 'y'
|
|
4318
|
+
command("rm -rf #{TRASH_DIR}/*")
|
|
4319
|
+
@undo_history.reject! { |op| op[:type] == 'delete' && op[:trash] }
|
|
4320
|
+
@pB.say("Trash emptied.".fg(204))
|
|
4321
|
+
sel = 0
|
|
4322
|
+
end
|
|
4323
|
+
end
|
|
4324
|
+
end
|
|
4082
4325
|
|
|
4083
|
-
|
|
4084
|
-
|
|
4326
|
+
@pR.update = true
|
|
4327
|
+
refresh
|
|
4085
4328
|
render
|
|
4086
4329
|
end
|
|
4087
4330
|
|
|
4331
|
+
def human_size(bytes) # {{{3
|
|
4332
|
+
if bytes >= 1_073_741_824
|
|
4333
|
+
"%.1fG" % (bytes / 1_073_741_824.0)
|
|
4334
|
+
elsif bytes >= 1_048_576
|
|
4335
|
+
"%.1fM" % (bytes / 1_048_576.0)
|
|
4336
|
+
elsif bytes >= 1024
|
|
4337
|
+
"%.1fK" % (bytes / 1024.0)
|
|
4338
|
+
else
|
|
4339
|
+
"#{bytes}B"
|
|
4340
|
+
end
|
|
4341
|
+
end
|
|
4342
|
+
|
|
4343
|
+
def time_ago(time) # {{{3
|
|
4344
|
+
seconds = Time.now - time
|
|
4345
|
+
if seconds < 60 then "now"
|
|
4346
|
+
elsif seconds < 3600 then "#{(seconds / 60).to_i}m"
|
|
4347
|
+
elsif seconds < 86400 then "#{(seconds / 3600).to_i}h"
|
|
4348
|
+
elsif seconds < 604800 then "#{(seconds / 86400).to_i}d"
|
|
4349
|
+
else "#{(seconds / 604800).to_i}w"
|
|
4350
|
+
end
|
|
4351
|
+
end
|
|
4352
|
+
|
|
4088
4353
|
def toggle_trash # {{{3
|
|
4089
4354
|
@trash = !@trash
|
|
4090
4355
|
@pB.say("Trash (~/.rtfm/trash) = #{@trash ? 'On' : 'Off'}")
|
|
@@ -4593,19 +4858,19 @@ def refresh_right # {{{3
|
|
|
4593
4858
|
end
|
|
4594
4859
|
|
|
4595
4860
|
def line_down_right # {{{3
|
|
4596
|
-
@pR.linedown; @pB.update = true
|
|
4861
|
+
@pR.update = true; @pR.linedown; @pR.update = false; @pB.update = true
|
|
4597
4862
|
end
|
|
4598
4863
|
|
|
4599
4864
|
def line_up_right # {{{3
|
|
4600
|
-
@pR.lineup;
|
|
4865
|
+
@pR.update = true; @pR.lineup; @pR.update = false; @pB.update = true
|
|
4601
4866
|
end
|
|
4602
4867
|
|
|
4603
4868
|
def page_down_right # {{{3
|
|
4604
|
-
@pR.pagedown; @pB.update = true
|
|
4869
|
+
@pR.update = true; @pR.pagedown; @pR.update = false; @pB.update = true
|
|
4605
4870
|
end
|
|
4606
4871
|
|
|
4607
4872
|
def page_up_right # {{{3
|
|
4608
|
-
@pR.pageup; @pB.update = true
|
|
4873
|
+
@pR.update = true; @pR.pageup; @pR.update = false; @pB.update = true
|
|
4609
4874
|
end
|
|
4610
4875
|
|
|
4611
4876
|
# CLIPBOARD COPY {{{2
|