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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e58685e75cad90ce6073efe92abe1fe018bf0371060e652dd86ccf6e7e41d252
4
- data.tar.gz: 6fd3b54ac0729a9111aedad4c48b3e50c6b9467547d62e49c14bc22015c58d13
3
+ metadata.gz: eb02bbf2f12ccc66afd6d2fbbef68f6e7a1a05f3d932e48a8143ae8df1a70d39
4
+ data.tar.gz: e1a1583ccab118308ecf16d898631dff1fcddedb3b46f7462ba2a8486cfad5d1
5
5
  SHA512:
6
- metadata.gz: 26cb3b44a3b43f90c326050c18c0809b4d689be8610a38c9c591c5bc0519cb197988fd6b63305d0620ab7dddd3f00ae848ed2f5716b166c31d2dd8eab0237719
7
- data.tar.gz: 76a5d98f72158d0cad178cf5b7c0d71e5ea7bea5bc51d06d2e23c2cbe3342e81e22508681f614f80f247e26394d299bf87765539e35b31461c27131c92a587db
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 architecture** - Custom preview handlers and key bindings
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` | Empty trash |
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 supports two types of plugins in `~/.rtfm/plugins/`:
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
- ### 1. Preview Handlers (`preview.rb`)
500
+ ### Plugin Manager (`V` key)
498
501
 
499
- Add custom file type previews:
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
- ```ruby
502
- # ~/.rtfm/plugins/preview.rb
508
+ ### Example Plugins
503
509
 
504
- # Syntax: ext1, ext2 = command with @s placeholder
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
- # Examples:
508
- txt, log = bat -n --color=always @s
509
- md = pandoc @s -t plain
510
- pdf = pdftotext -f 1 -l 4 @s -
511
- json = jq . @s
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
- ### 2. Custom Key Bindings (`keys.rb`)
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
- Add or override key bindings:
534
+ A plugin is a simple Ruby file with metadata comments and KEYMAP bindings:
517
535
 
518
536
  ```ruby
519
- # ~/.rtfm/plugins/keys.rb
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
- # Add new key binding
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
- def my_custom_action(_chr)
525
- @pB.say("Custom action triggered!")
526
- # Use @pL, @pR, @selected, etc.
527
- end
558
+ ### Preview Handlers (`preview.rb`)
528
559
 
529
- # Git commit shortcut example
530
- KEYMAP['C-G'] = :git_commit
560
+ Custom file type previews are configured separately in `~/.rtfm/preview.rb`:
531
561
 
532
- def git_commit
533
- message = @pCmd.ask('Commit message: ', '')
534
- shellexec("git add . && git commit -m '#{message}' && git push")
535
- end
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, show errors
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.0' # File picker mode (--pick) for integration with other tools
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 = Empty trash directory
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
- @pB.say(" Really empty Trash (~/.rtfm/trash)? (press 'y')")
4081
- return unless getchr == 'y'
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
- command("rm -rf #{TRASH_DIR}/*")
4084
- @pB.say('Trash is now empty')
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; @pB.update = true
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