rtfm-filemanager 8.1.3 → 8.2.0
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 +11 -0
- data/README.md +81 -37
- data/bin/rtfm +128 -1
- data/examples/bookmarks.rb +137 -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 +11 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ef7c28629f7646f129d96e76afd871a7ab6030e0adb9983055a247ccc3992ecc
|
|
4
|
+
data.tar.gz: b1c7f85c145f532f5e3175c5fb2f4642d134149d147fc06b4a26389f25d1f42c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9207a4adaeed8a14f3a7a78a70fafdcba2318635605406168679aefe6f2e859cedc7c4053ebcf0d08f610415874df41f061fedad1fd41955cbcc7ae1eb2206a9
|
|
7
|
+
data.tar.gz: 1c8b70f11a2436b5beedebf1b0a510395e62e93a99ed1050192f381097693816731864f46c86a22eeebb8139ae9bb09367625c01ad9856142cffb2b271fdb53d
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,17 @@ 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.0] - 2026-03-15
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **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
|
|
12
|
+
- **Plugin manager** (`V` key) - Enable/disable plugins live without restarting RTFM. Press `?` on any plugin to view its built-in help text
|
|
13
|
+
- **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
|
|
14
|
+
- **Git plugin** (`Ctrl-G`) - Quick git menu: status, diff, commit+push, and log, all from within RTFM
|
|
15
|
+
- **Bookmarks plugin** (`F6`) - Unlimited directory bookmarks with fuzzy filtering, complementing the built-in single-letter marks
|
|
16
|
+
- **Notes plugin** (`F5`) - Attach text notes to any file or directory, stored in `~/.rtfm/notes/`
|
|
17
|
+
- **Opener plugin** (`RIGHT`/`l`) - Define custom programs for specific file extensions via a simple hash
|
|
18
|
+
|
|
8
19
|
## [8.1.3] - 2026-03-13
|
|
9
20
|
|
|
10
21
|
### 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
|
|
|
@@ -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,75 @@ 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/
|
|
512
518
|
```
|
|
513
519
|
|
|
514
|
-
|
|
520
|
+
| Plugin | Key | Description |
|
|
521
|
+
|--------|-----|-------------|
|
|
522
|
+
| **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 |
|
|
523
|
+
| **Git** | `Ctrl-G` | Git operations menu: status, diff, commit+push, log. Output shown in right pane |
|
|
524
|
+
| **Bookmarks** | `F6` | Unlimited directory bookmarks with fuzzy filtering. Add, delete, and jump to bookmarked directories. Complements the built-in single-letter marks (m/') |
|
|
525
|
+
| **Notes** | `F5` | Attach text notes to any file or directory. View, edit, or delete notes. Stored in `~/.rtfm/notes/` |
|
|
526
|
+
| **Opener** | `RIGHT`/`l` | Custom file openers by extension. Configure a hash of extension-to-command mappings (e.g., `.hl` files open in hyperlist) |
|
|
527
|
+
|
|
528
|
+
### Writing Your Own Plugins
|
|
515
529
|
|
|
516
|
-
|
|
530
|
+
A plugin is a simple Ruby file with metadata comments and KEYMAP bindings:
|
|
517
531
|
|
|
518
532
|
```ruby
|
|
519
|
-
#
|
|
533
|
+
# @name: My Plugin
|
|
534
|
+
# @description: What it does
|
|
535
|
+
# @key: X
|
|
536
|
+
|
|
537
|
+
KEYMAP['X'] = :my_action
|
|
538
|
+
|
|
539
|
+
# Optional: register help text shown by ? in plugin manager
|
|
540
|
+
PLUGIN_HELP['My Plugin'] = <<~HELP
|
|
541
|
+
This is the help text for my plugin.
|
|
542
|
+
Shown when pressing ? in the plugin manager.
|
|
543
|
+
HELP
|
|
544
|
+
|
|
545
|
+
def my_action
|
|
546
|
+
clear_image
|
|
547
|
+
@pR.update = true
|
|
548
|
+
@pR.say("Hello from my plugin!")
|
|
549
|
+
end
|
|
550
|
+
```
|
|
520
551
|
|
|
521
|
-
|
|
522
|
-
KEYMAP['Z'] = :my_custom_action
|
|
552
|
+
Plugins can also be defined in `~/.rtfm/keys.rb` for personal bindings that don't need the enable/disable mechanism.
|
|
523
553
|
|
|
524
|
-
|
|
525
|
-
@pB.say("Custom action triggered!")
|
|
526
|
-
# Use @pL, @pR, @selected, etc.
|
|
527
|
-
end
|
|
554
|
+
### Preview Handlers (`preview.rb`)
|
|
528
555
|
|
|
529
|
-
|
|
530
|
-
KEYMAP['C-G'] = :git_commit
|
|
556
|
+
Custom file type previews are configured separately in `~/.rtfm/preview.rb`:
|
|
531
557
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
558
|
+
```ruby
|
|
559
|
+
# Syntax: ext1, ext2 = command with @s placeholder
|
|
560
|
+
txt, log = bat -n --color=always @s
|
|
561
|
+
md = pandoc @s -t plain
|
|
562
|
+
pdf = pdftotext -f 1 -l 4 @s -
|
|
563
|
+
json = jq . @s
|
|
536
564
|
```
|
|
537
565
|
|
|
538
|
-
### Available Variables
|
|
566
|
+
### Available Variables for Plugin Authors
|
|
539
567
|
|
|
540
568
|
| Variable | Description |
|
|
541
569
|
|----------|-------------|
|
|
@@ -543,22 +571,33 @@ end
|
|
|
543
571
|
| `@pL` | Left pane (file list) |
|
|
544
572
|
| `@pR` | Right pane (preview) |
|
|
545
573
|
| `@pB` | Bottom pane (status) |
|
|
546
|
-
| `@pCmd` | Command prompt |
|
|
574
|
+
| `@pCmd` | Command prompt (use `.ask(prompt, default)` for input) |
|
|
547
575
|
| `@selected` | Currently selected file/dir |
|
|
548
576
|
| `@tagged` | Array of tagged items |
|
|
549
577
|
| `@external_program_running` | Set true when launching TUI programs |
|
|
578
|
+
| `PLUGIN_HELP` | Hash to register help text (keyed by plugin name) |
|
|
550
579
|
|
|
551
580
|
### Plugin Helper Functions
|
|
552
581
|
|
|
553
582
|
```ruby
|
|
554
|
-
# Capture command output
|
|
583
|
+
# Capture command output as string
|
|
555
584
|
output = command("ls -la", timeout: 5)
|
|
556
585
|
|
|
557
|
-
# Run command
|
|
558
|
-
shell("mv file1 file2", background: false)
|
|
559
|
-
|
|
560
|
-
# Run and show both stdout/stderr in right pane
|
|
586
|
+
# Run command interactively (full terminal)
|
|
561
587
|
shellexec("grep -r pattern .")
|
|
588
|
+
|
|
589
|
+
# Read a keypress
|
|
590
|
+
chr = getchr
|
|
591
|
+
|
|
592
|
+
# Clear any displayed image
|
|
593
|
+
clear_image
|
|
594
|
+
|
|
595
|
+
# Text formatting
|
|
596
|
+
"text".fg(112) # foreground color (0-255)
|
|
597
|
+
"text".bg(236) # background color
|
|
598
|
+
"text".b # bold
|
|
599
|
+
"text".u # underline
|
|
600
|
+
"text".r # reverse
|
|
562
601
|
```
|
|
563
602
|
|
|
564
603
|
---
|
|
@@ -650,6 +689,11 @@ Best image experience with: kitty, urxvt, xterm, mlterm, foot
|
|
|
650
689
|
|
|
651
690
|
## Latest Updates
|
|
652
691
|
|
|
692
|
+
### Version 8.2 Highlights
|
|
693
|
+
|
|
694
|
+
- **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 `?`.
|
|
695
|
+
- **Five example plugins** - Settings editor, git operations, directory bookmarks, file notes, and custom file openers. Copy from `examples/` to `~/.rtfm/plugins/` to use.
|
|
696
|
+
|
|
653
697
|
### Version 8.1 Highlights
|
|
654
698
|
|
|
655
699
|
- **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.
|
|
21
|
+
@version = '8.2.0' # Plugin system with live enable/disable and built-in plugin manager
|
|
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
|
|
|
@@ -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
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# @name: Bookmarks
|
|
2
|
+
# @description: Quick directory bookmarks with fuzzy jump
|
|
3
|
+
# @key: F6
|
|
4
|
+
|
|
5
|
+
BOOKMARKS_FILE = File.expand_path('~/.rtfm/bookmarks.txt')
|
|
6
|
+
|
|
7
|
+
KEYMAP['F6'] = :bookmark_menu
|
|
8
|
+
|
|
9
|
+
PLUGIN_HELP['Bookmarks'] = <<~HELP
|
|
10
|
+
Quick directory bookmarks with fuzzy filtering.
|
|
11
|
+
|
|
12
|
+
Press F6 to open the bookmark manager.
|
|
13
|
+
|
|
14
|
+
#{"Commands:".b}
|
|
15
|
+
a Add current directory to bookmarks
|
|
16
|
+
d Delete selected bookmark
|
|
17
|
+
j/k Navigate up/down
|
|
18
|
+
ENTER Jump to selected directory
|
|
19
|
+
/ Filter bookmarks (fuzzy search)
|
|
20
|
+
q/ESC Close
|
|
21
|
+
|
|
22
|
+
#{"Difference from marks:".b}
|
|
23
|
+
Marks (m/') are single-letter slots (a-z) that
|
|
24
|
+
save a directory path for quick recall. You get
|
|
25
|
+
26 slots max, and must remember which letter
|
|
26
|
+
maps to which directory.
|
|
27
|
+
|
|
28
|
+
Bookmarks are an unlimited named list of
|
|
29
|
+
directories with fuzzy filtering. Better for
|
|
30
|
+
large collections of frequently visited paths.
|
|
31
|
+
They persist in ~/.rtfm/bookmarks.txt.
|
|
32
|
+
HELP
|
|
33
|
+
|
|
34
|
+
def load_bookmarks
|
|
35
|
+
return [] unless File.exist?(BOOKMARKS_FILE)
|
|
36
|
+
File.readlines(BOOKMARKS_FILE).map(&:strip).reject(&:empty?)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def save_bookmarks(bookmarks)
|
|
40
|
+
File.write(BOOKMARKS_FILE, bookmarks.join("\n") + "\n")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def bookmark_menu
|
|
44
|
+
clear_image
|
|
45
|
+
|
|
46
|
+
sel = 0
|
|
47
|
+
filter = nil
|
|
48
|
+
|
|
49
|
+
loop do
|
|
50
|
+
bookmarks = load_bookmarks
|
|
51
|
+
visible = filter ? bookmarks.select { |b| b.downcase.include?(filter.downcase) } : bookmarks
|
|
52
|
+
|
|
53
|
+
lines = []
|
|
54
|
+
lines << "Bookmarks".b.fg(254)
|
|
55
|
+
lines << "filter: #{filter}".fg(240) if filter
|
|
56
|
+
lines << ""
|
|
57
|
+
|
|
58
|
+
if visible.empty?
|
|
59
|
+
lines << (filter ? "No matches." : "No bookmarks yet.").fg(240)
|
|
60
|
+
else
|
|
61
|
+
# Find common prefix to dim shared parts
|
|
62
|
+
common = ""
|
|
63
|
+
if visible.size > 1
|
|
64
|
+
parts = visible.map { |b| b.split('/') }
|
|
65
|
+
min_len = parts.map(&:size).min
|
|
66
|
+
min_len.times do |i|
|
|
67
|
+
break unless parts.all? { |p| p[i] == parts[0][i] }
|
|
68
|
+
common += parts[0][i] + '/'
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
visible.each_with_index do |path, i|
|
|
73
|
+
idx = bookmarks.index(path)
|
|
74
|
+
prefix = common.empty? ? "" : path[0, common.length].fg(240)
|
|
75
|
+
suffix = common.empty? ? path.fg(112) : path[common.length..].fg(112)
|
|
76
|
+
line = "#{idx.to_s.rjust(2).fg(240)} #{prefix}#{suffix}"
|
|
77
|
+
line = i == sel ? line.u : line
|
|
78
|
+
lines << line
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
lines << ""
|
|
83
|
+
lines << "a".b.fg(112) + ":add " + "d".b.fg(112) + ":del " +
|
|
84
|
+
"/".b.fg(112) + ":filter " + "ENTER".b.fg(112) + ":jump " +
|
|
85
|
+
"q".b.fg(112) + ":close"
|
|
86
|
+
|
|
87
|
+
@pR.update = true
|
|
88
|
+
@pR.say(lines.join("\n"))
|
|
89
|
+
|
|
90
|
+
chr = getchr
|
|
91
|
+
case chr
|
|
92
|
+
when 'q', 'ESC'
|
|
93
|
+
break
|
|
94
|
+
when 'j', 'DOWN'
|
|
95
|
+
sel = visible.empty? ? 0 : (sel + 1) % visible.size
|
|
96
|
+
when 'k', 'UP'
|
|
97
|
+
sel = visible.empty? ? 0 : (sel - 1) % visible.size
|
|
98
|
+
when 'a'
|
|
99
|
+
cwd = Dir.pwd
|
|
100
|
+
unless bookmarks.include?(cwd)
|
|
101
|
+
bookmarks << cwd
|
|
102
|
+
save_bookmarks(bookmarks)
|
|
103
|
+
@pB.say("Bookmarked: #{cwd}")
|
|
104
|
+
else
|
|
105
|
+
@pB.say("Already bookmarked.")
|
|
106
|
+
end
|
|
107
|
+
when 'd'
|
|
108
|
+
unless visible.empty?
|
|
109
|
+
path = visible[sel]
|
|
110
|
+
bookmarks.delete(path)
|
|
111
|
+
save_bookmarks(bookmarks)
|
|
112
|
+
sel = [sel, visible.size - 2].max
|
|
113
|
+
sel = 0 if sel < 0
|
|
114
|
+
@pB.say("Removed: #{path}")
|
|
115
|
+
end
|
|
116
|
+
when 'ENTER'
|
|
117
|
+
unless visible.empty?
|
|
118
|
+
path = visible[sel]
|
|
119
|
+
if Dir.exist?(path)
|
|
120
|
+
Dir.chdir(path)
|
|
121
|
+
@pB.say("Jumped to: #{path}")
|
|
122
|
+
break
|
|
123
|
+
else
|
|
124
|
+
@pB.say("Directory not found: #{path}")
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
when '/'
|
|
128
|
+
input = @pCmd.ask('Filter: ', filter || '')
|
|
129
|
+
filter = input.strip.empty? ? nil : input.strip
|
|
130
|
+
sel = 0
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
@pR.update = true
|
|
135
|
+
refresh
|
|
136
|
+
render
|
|
137
|
+
end
|
data/examples/git.rb
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# @name: Git
|
|
2
|
+
# @description: Git operations (status, commit, push, diff)
|
|
3
|
+
# @key: C-G
|
|
4
|
+
|
|
5
|
+
KEYMAP['C-G'] = :git_menu
|
|
6
|
+
|
|
7
|
+
PLUGIN_HELP['Git'] = <<~HELP
|
|
8
|
+
Quick git operations for the current directory.
|
|
9
|
+
|
|
10
|
+
Press Ctrl-G to open the git menu.
|
|
11
|
+
|
|
12
|
+
#{"Commands:".b}
|
|
13
|
+
s Show git status
|
|
14
|
+
d Show git diff
|
|
15
|
+
c Stage all, commit (prompts for message), push
|
|
16
|
+
l Show last 20 commits (git log --oneline)
|
|
17
|
+
q Close menu
|
|
18
|
+
|
|
19
|
+
All output displays in the right pane.
|
|
20
|
+
The commit command runs interactively so you
|
|
21
|
+
can see push progress and any errors.
|
|
22
|
+
HELP
|
|
23
|
+
|
|
24
|
+
def git_menu
|
|
25
|
+
clear_image
|
|
26
|
+
|
|
27
|
+
loop do
|
|
28
|
+
lines = []
|
|
29
|
+
lines << "Git".b.fg(254)
|
|
30
|
+
lines << ""
|
|
31
|
+
lines << "s".b.fg(112) + " git status"
|
|
32
|
+
lines << "d".b.fg(112) + " git diff"
|
|
33
|
+
lines << "c".b.fg(112) + " git add + commit + push"
|
|
34
|
+
lines << "l".b.fg(112) + " git log (last 20)"
|
|
35
|
+
lines << ""
|
|
36
|
+
lines << "q/ESC: close".fg(240)
|
|
37
|
+
|
|
38
|
+
@pR.update = true
|
|
39
|
+
@pR.say(lines.join("\n"))
|
|
40
|
+
|
|
41
|
+
chr = getchr
|
|
42
|
+
case chr
|
|
43
|
+
when 's'
|
|
44
|
+
output = command("git status 2>&1")
|
|
45
|
+
@pR.update = true
|
|
46
|
+
@pR.say("git status".b.fg(254) + "\n\n" + output)
|
|
47
|
+
when 'd'
|
|
48
|
+
output = command("git diff 2>&1")
|
|
49
|
+
output = "No changes." if output.strip.empty?
|
|
50
|
+
@pR.update = true
|
|
51
|
+
@pR.say("git diff".b.fg(254) + "\n\n" + output)
|
|
52
|
+
when 'c'
|
|
53
|
+
message = @pCmd.ask('Commit message: ', '')
|
|
54
|
+
if message.strip.empty?
|
|
55
|
+
@pB.say("Aborted: empty commit message.")
|
|
56
|
+
next
|
|
57
|
+
end
|
|
58
|
+
@pB.say("Committing and pushing...")
|
|
59
|
+
shellexec("git add . && git commit -m '#{message}' && git push", timeout: 20)
|
|
60
|
+
@pB.full_refresh
|
|
61
|
+
when 'l'
|
|
62
|
+
output = command("git log --oneline -20 2>&1")
|
|
63
|
+
@pR.update = true
|
|
64
|
+
@pR.say("git log".b.fg(254) + "\n\n" + output)
|
|
65
|
+
when 'q', 'ESC'
|
|
66
|
+
break
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
@pR.update = true
|
|
71
|
+
refresh
|
|
72
|
+
render
|
|
73
|
+
end
|
data/examples/notes.rb
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# @name: Notes
|
|
2
|
+
# @description: Attach notes to files/directories, shown in right pane
|
|
3
|
+
# @key: F5
|
|
4
|
+
|
|
5
|
+
require 'fileutils'
|
|
6
|
+
|
|
7
|
+
NOTES_DIR = File.expand_path('~/.rtfm/notes')
|
|
8
|
+
FileUtils.mkdir_p(NOTES_DIR)
|
|
9
|
+
|
|
10
|
+
KEYMAP['F5'] = :toggle_note
|
|
11
|
+
|
|
12
|
+
PLUGIN_HELP['Notes'] = <<~HELP
|
|
13
|
+
Attach text notes to any file or directory.
|
|
14
|
+
|
|
15
|
+
Press F5 on any selected item to view, create,
|
|
16
|
+
edit, or delete its note.
|
|
17
|
+
|
|
18
|
+
#{"How it works:".b}
|
|
19
|
+
If no note exists, you're prompted to create one.
|
|
20
|
+
If a note exists, it shows in the right pane
|
|
21
|
+
with options to edit or delete.
|
|
22
|
+
|
|
23
|
+
#{"Commands (when viewing a note):".b}
|
|
24
|
+
e Edit the note
|
|
25
|
+
d Delete the note
|
|
26
|
+
q Close
|
|
27
|
+
|
|
28
|
+
#{"Difference from marks:".b}
|
|
29
|
+
Marks (m/') are single-letter bookmarks for
|
|
30
|
+
quick directory jumping. Notes are free-text
|
|
31
|
+
annotations attached to specific files or
|
|
32
|
+
directories, useful for reminders, TODO items,
|
|
33
|
+
or documentation.
|
|
34
|
+
|
|
35
|
+
Notes are stored in ~/.rtfm/notes/ as text files.
|
|
36
|
+
HELP
|
|
37
|
+
|
|
38
|
+
def note_path_for(file)
|
|
39
|
+
File.join(NOTES_DIR, file.gsub('/', '%') + '.txt')
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def toggle_note
|
|
43
|
+
clear_image
|
|
44
|
+
file = @selected
|
|
45
|
+
npath = note_path_for(file)
|
|
46
|
+
has_note = File.exist?(npath)
|
|
47
|
+
|
|
48
|
+
if has_note
|
|
49
|
+
note = File.read(npath)
|
|
50
|
+
loop do
|
|
51
|
+
lines = []
|
|
52
|
+
lines << "Note for:".b.fg(254)
|
|
53
|
+
lines << File.basename(file).fg(112)
|
|
54
|
+
lines << ""
|
|
55
|
+
lines << note
|
|
56
|
+
lines << ""
|
|
57
|
+
lines << "e".b.fg(112) + " edit note"
|
|
58
|
+
lines << "d".b.fg(112) + " delete note"
|
|
59
|
+
lines << "q/ESC: close".fg(240)
|
|
60
|
+
|
|
61
|
+
@pR.update = true
|
|
62
|
+
@pR.say(lines.join("\n"))
|
|
63
|
+
|
|
64
|
+
chr = getchr
|
|
65
|
+
case chr
|
|
66
|
+
when 'e'
|
|
67
|
+
input = @pCmd.ask('Note: ', note)
|
|
68
|
+
unless input.strip.empty?
|
|
69
|
+
File.write(npath, input)
|
|
70
|
+
note = input
|
|
71
|
+
@pB.say('Note updated.')
|
|
72
|
+
end
|
|
73
|
+
when 'd'
|
|
74
|
+
File.delete(npath)
|
|
75
|
+
@pB.say('Note deleted.')
|
|
76
|
+
break
|
|
77
|
+
when 'q', 'ESC'
|
|
78
|
+
break
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
else
|
|
82
|
+
input = @pCmd.ask('Note: ', '')
|
|
83
|
+
unless input.strip.empty?
|
|
84
|
+
File.write(npath, input)
|
|
85
|
+
@pB.say('Note saved.')
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
@pR.update = true
|
|
90
|
+
refresh
|
|
91
|
+
render
|
|
92
|
+
end
|
data/examples/opener.rb
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# @name: Opener
|
|
2
|
+
# @description: Custom file openers by extension (override default open behavior)
|
|
3
|
+
# @key: RIGHT/l (overrides move_right)
|
|
4
|
+
|
|
5
|
+
require 'shellwords'
|
|
6
|
+
|
|
7
|
+
# User-configurable openers hash:
|
|
8
|
+
# Extension => command string (use %f for file path)
|
|
9
|
+
PLUGIN_HELP['Opener'] = <<~HELP
|
|
10
|
+
Custom file openers by extension.
|
|
11
|
+
|
|
12
|
+
Overrides RIGHT/l to check selected files against
|
|
13
|
+
a list of custom openers before using the default
|
|
14
|
+
open behavior.
|
|
15
|
+
|
|
16
|
+
#{"Configuration:".b}
|
|
17
|
+
Edit the CUSTOM_OPENERS hash in the plugin file
|
|
18
|
+
(~/.rtfm/plugins/opener.rb):
|
|
19
|
+
|
|
20
|
+
CUSTOM_OPENERS = {
|
|
21
|
+
'.md' => 'glow %f',
|
|
22
|
+
'.pdf' => 'zathura %f',
|
|
23
|
+
'.hl' => 'hyperlist %f',
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
Use %f as placeholder for the file path.
|
|
27
|
+
|
|
28
|
+
#{"How it works:".b}
|
|
29
|
+
When you press RIGHT or l on a file, the plugin
|
|
30
|
+
checks if its extension matches any entry in
|
|
31
|
+
CUSTOM_OPENERS. If yes, it launches that program
|
|
32
|
+
interactively. If no match, the normal RTFM
|
|
33
|
+
open behavior is used.
|
|
34
|
+
HELP
|
|
35
|
+
|
|
36
|
+
CUSTOM_OPENERS = {
|
|
37
|
+
# '.md' => 'glow %f', # Example: markdown viewer
|
|
38
|
+
# '.pdf' => 'zathura %f', # Example: PDF viewer
|
|
39
|
+
# '.csv' => 'visidata %f', # Example: CSV explorer
|
|
40
|
+
# '.hl' => 'hyperlist %f', # Example: HyperList files
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
def custom_open
|
|
44
|
+
# Check if the selected file matches any registered extension
|
|
45
|
+
ext = CUSTOM_OPENERS.keys.find { |e| @selected.end_with?(e) }
|
|
46
|
+
|
|
47
|
+
unless ext
|
|
48
|
+
# No custom opener, fall back to original move_right
|
|
49
|
+
move_right
|
|
50
|
+
return
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
cmd = CUSTOM_OPENERS[ext].gsub('%f', Shellwords.escape(@selected))
|
|
54
|
+
|
|
55
|
+
# Launch the program interactively (same pattern as RTFM's interactive handling)
|
|
56
|
+
@external_program_running = true
|
|
57
|
+
system("stty #{ORIG_STTY} < /dev/tty")
|
|
58
|
+
system('stty sane < /dev/tty')
|
|
59
|
+
system('clear < /dev/tty > /dev/tty')
|
|
60
|
+
Cursor.show
|
|
61
|
+
|
|
62
|
+
pid = Process.spawn(cmd,
|
|
63
|
+
in: '/dev/tty',
|
|
64
|
+
out: '/dev/tty',
|
|
65
|
+
err: '/dev/tty')
|
|
66
|
+
begin
|
|
67
|
+
Process.wait(pid)
|
|
68
|
+
rescue Interrupt
|
|
69
|
+
Process.kill('TERM', pid) rescue nil
|
|
70
|
+
retry
|
|
71
|
+
ensure
|
|
72
|
+
@external_program_running = false
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Restore RTFM's terminal state
|
|
76
|
+
system('stty raw -echo isig < /dev/tty')
|
|
77
|
+
$stdin.raw!
|
|
78
|
+
$stdin.echo = false
|
|
79
|
+
Cursor.hide
|
|
80
|
+
Rcurses.clear_screen
|
|
81
|
+
refresh
|
|
82
|
+
render
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
KEYMAP['RIGHT'] = :custom_open
|
|
86
|
+
KEYMAP['l'] = :custom_open
|
|
87
|
+
KEYMAP['C-RIGHT'] = :custom_open
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# @name: Settings
|
|
2
|
+
# @description: Interactive settings editor (colors, toggles, paths)
|
|
3
|
+
# @key: C
|
|
4
|
+
|
|
5
|
+
KEYMAP['C'] = :show_settings
|
|
6
|
+
|
|
7
|
+
PLUGIN_HELP['Settings'] = <<~HELP
|
|
8
|
+
Interactive editor for RTFM settings that don't
|
|
9
|
+
have their own dedicated keys.
|
|
10
|
+
|
|
11
|
+
Overrides the default 'C' (show config) key.
|
|
12
|
+
|
|
13
|
+
#{"Navigation:".b}
|
|
14
|
+
j/k or UP/DOWN Navigate settings
|
|
15
|
+
LEFT/RIGHT Toggle booleans, adjust colors +-1
|
|
16
|
+
H/L Adjust colors +-10
|
|
17
|
+
ENTER Edit text fields or type color number
|
|
18
|
+
q/ESC Save and close
|
|
19
|
+
|
|
20
|
+
#{"Settings included:".b}
|
|
21
|
+
Trash mode, run-mailcap, interactive programs,
|
|
22
|
+
custom ls flags, OpenAI key/model, and all
|
|
23
|
+
pane colors (top, bottom, search, command,
|
|
24
|
+
ruby, AI, SSH bars).
|
|
25
|
+
|
|
26
|
+
Changes are saved to ~/.rtfm/conf on close.
|
|
27
|
+
Color changes apply immediately.
|
|
28
|
+
HELP
|
|
29
|
+
|
|
30
|
+
def show_settings
|
|
31
|
+
clear_image
|
|
32
|
+
|
|
33
|
+
# Settings definitions: [label, variable, type, options]
|
|
34
|
+
# Types: :bool, :cycle, :int_range, :text
|
|
35
|
+
settings = [
|
|
36
|
+
['Trash (move to trash)', :@trash, :bool],
|
|
37
|
+
['Use run-mailcap', :@runmailcap, :bool],
|
|
38
|
+
['Interactive programs', :@interactive, :text],
|
|
39
|
+
['Custom ls flags', :@lsuser, :text],
|
|
40
|
+
['OpenAI API key', :@ai, :text_masked],
|
|
41
|
+
['OpenAI model', :@aimodel, :text],
|
|
42
|
+
['Top bar color', :@topcolor, :color],
|
|
43
|
+
['Bottom bar color', :@bottomcolor, :color],
|
|
44
|
+
['Search bar color', :@searchcolor, :color],
|
|
45
|
+
['Command bar color', :@cmdcolor, :color],
|
|
46
|
+
['Ruby bar color', :@rubycolor, :color],
|
|
47
|
+
['AI bar color', :@aicolor, :color],
|
|
48
|
+
['SSH bar color', :@sshcolor, :color],
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
sel = 0
|
|
52
|
+
label_w = settings.map { |s| s[0].length }.max + 2
|
|
53
|
+
|
|
54
|
+
loop do
|
|
55
|
+
# Build display
|
|
56
|
+
lines = []
|
|
57
|
+
lines << "Settings".b.fg(254)
|
|
58
|
+
lines << ""
|
|
59
|
+
|
|
60
|
+
settings.each_with_index do |(label, var, type, _opts), i|
|
|
61
|
+
val = instance_variable_get(var)
|
|
62
|
+
display = case type
|
|
63
|
+
when :bool
|
|
64
|
+
val ? "Yes" : "No"
|
|
65
|
+
when :color
|
|
66
|
+
swatch = " #{val} ".bg(val.to_i).fg(val.to_i > 128 ? 0 : 255)
|
|
67
|
+
"#{swatch} #{val}"
|
|
68
|
+
when :text_masked
|
|
69
|
+
val.to_s.length > 8 ? val.to_s[0..3] + "..." + val.to_s[-4..] : val.to_s
|
|
70
|
+
else
|
|
71
|
+
val.to_s
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
pad = label_w - label.length
|
|
75
|
+
line = "#{label}#{' ' * pad}#{display}"
|
|
76
|
+
line = i == sel ? line.u : line
|
|
77
|
+
lines << line
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
lines << ""
|
|
81
|
+
lines << "j/k:move LEFT/RIGHT:change ENTER:edit q:close".fg(240)
|
|
82
|
+
|
|
83
|
+
@pR.update = true
|
|
84
|
+
@pR.say(lines.join("\n"))
|
|
85
|
+
|
|
86
|
+
chr = getchr
|
|
87
|
+
case chr
|
|
88
|
+
when 'q', 'ESC'
|
|
89
|
+
break
|
|
90
|
+
when 'j', 'DOWN'
|
|
91
|
+
sel = (sel + 1) % settings.size
|
|
92
|
+
when 'k', 'UP'
|
|
93
|
+
sel = (sel - 1) % settings.size
|
|
94
|
+
when 'RIGHT', 'LEFT', 'ENTER'
|
|
95
|
+
label, var, type, _opts = settings[sel]
|
|
96
|
+
val = instance_variable_get(var)
|
|
97
|
+
|
|
98
|
+
case type
|
|
99
|
+
when :bool
|
|
100
|
+
instance_variable_set(var, !val)
|
|
101
|
+
when :color
|
|
102
|
+
delta = chr == 'RIGHT' ? 1 : chr == 'LEFT' ? -1 : 0
|
|
103
|
+
if chr == 'ENTER'
|
|
104
|
+
input = @pCmd.ask("#{label} (0-255): ", val.to_s).strip
|
|
105
|
+
new_val = input.to_i
|
|
106
|
+
instance_variable_set(var, new_val.clamp(0, 255))
|
|
107
|
+
else
|
|
108
|
+
instance_variable_set(var, (val.to_i + delta).clamp(0, 255))
|
|
109
|
+
end
|
|
110
|
+
# Apply color changes immediately
|
|
111
|
+
apply_color(var)
|
|
112
|
+
when :text, :text_masked
|
|
113
|
+
input = @pCmd.ask("#{label}: ", val.to_s)
|
|
114
|
+
instance_variable_set(var, input)
|
|
115
|
+
end
|
|
116
|
+
when 'H'
|
|
117
|
+
# Jump -10 for color settings
|
|
118
|
+
_label, var, type, _opts = settings[sel]
|
|
119
|
+
if type == :color
|
|
120
|
+
val = instance_variable_get(var)
|
|
121
|
+
instance_variable_set(var, (val.to_i - 10).clamp(0, 255))
|
|
122
|
+
apply_color(var)
|
|
123
|
+
end
|
|
124
|
+
when 'L'
|
|
125
|
+
# Jump +10 for color settings
|
|
126
|
+
_label, var, type, _opts = settings[sel]
|
|
127
|
+
if type == :color
|
|
128
|
+
val = instance_variable_get(var)
|
|
129
|
+
instance_variable_set(var, (val.to_i + 10).clamp(0, 255))
|
|
130
|
+
apply_color(var)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Save all settings to config
|
|
136
|
+
save_settings(settings)
|
|
137
|
+
@pR.update = true
|
|
138
|
+
refresh
|
|
139
|
+
render
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def apply_color(var)
|
|
143
|
+
case var
|
|
144
|
+
when :@topcolor
|
|
145
|
+
@pT.bg = @topcolor
|
|
146
|
+
@pT.update = true
|
|
147
|
+
when :@bottomcolor
|
|
148
|
+
@pB.bg = @bottomcolor
|
|
149
|
+
@pB.update = true
|
|
150
|
+
when :@searchcolor
|
|
151
|
+
@pSearch.bg = @searchcolor
|
|
152
|
+
when :@cmdcolor
|
|
153
|
+
@pCmd.bg = @cmdcolor
|
|
154
|
+
when :@rubycolor
|
|
155
|
+
@pRuby.bg = @rubycolor
|
|
156
|
+
when :@aicolor
|
|
157
|
+
@pAI.bg = @aicolor
|
|
158
|
+
when :@sshcolor
|
|
159
|
+
@pSsh.bg = @sshcolor
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def save_settings(settings)
|
|
164
|
+
@conf = @conf.dup
|
|
165
|
+
settings.each do |_label, var, type, _opts|
|
|
166
|
+
val = instance_variable_get(var)
|
|
167
|
+
name = var.to_s.sub('@', '')
|
|
168
|
+
line = case type
|
|
169
|
+
when :bool
|
|
170
|
+
"@#{name} = #{val}"
|
|
171
|
+
when :color
|
|
172
|
+
"@#{name} = #{val}"
|
|
173
|
+
when :text, :text_masked
|
|
174
|
+
"@#{name} = '#{val}'"
|
|
175
|
+
end
|
|
176
|
+
regex = /^@#{Regexp.escape(name)}\b.*$/
|
|
177
|
+
if @conf.match?(regex)
|
|
178
|
+
@conf.sub!(regex, line)
|
|
179
|
+
else
|
|
180
|
+
@conf << "\n" unless @conf.end_with?("\n")
|
|
181
|
+
@conf << line << "\n"
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
File.write(CONFIG_FILE, @conf)
|
|
185
|
+
@pB.say('Settings saved to ~/.rtfm/conf')
|
|
186
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rtfm-filemanager
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 8.
|
|
4
|
+
version: 8.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Geir Isene
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-03-
|
|
11
|
+
date: 2026-03-15 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rcurses
|
|
@@ -66,13 +66,12 @@ dependencies:
|
|
|
66
66
|
- - "~>"
|
|
67
67
|
- !ruby/object:Gem::Version
|
|
68
68
|
version: '7.4'
|
|
69
|
-
description: '
|
|
70
|
-
delete, add, move), async background file operations, scrollable diff viewer with
|
|
71
|
-
side-by-side mode. A full featured terminal browser with syntax highlighted files,
|
|
69
|
+
description: 'A full featured terminal file manager with syntax highlighted files,
|
|
72
70
|
images shown in the terminal, videos thumbnailed, etc. Features include remote SSH/SFTP
|
|
73
71
|
browsing, interactive SSH shell, comprehensive undo system, OpenAI integration,
|
|
74
|
-
bookmarks, and much more.
|
|
75
|
-
|
|
72
|
+
bookmarks, archive browsing, and much more. v8.2: Plugin system with live enable/disable,
|
|
73
|
+
built-in plugin manager (V key), and example plugins (settings editor, git operations,
|
|
74
|
+
bookmarks, notes, custom file openers).'
|
|
76
75
|
email: g@isene.com
|
|
77
76
|
executables:
|
|
78
77
|
- rtfm
|
|
@@ -89,7 +88,12 @@ files:
|
|
|
89
88
|
- docs/plugins.md
|
|
90
89
|
- docs/remote-browsing.md
|
|
91
90
|
- docs/troubleshooting.md
|
|
91
|
+
- examples/bookmarks.rb
|
|
92
|
+
- examples/git.rb
|
|
93
|
+
- examples/notes.rb
|
|
94
|
+
- examples/opener.rb
|
|
92
95
|
- examples/rtfm.conf
|
|
96
|
+
- examples/settings.rb
|
|
93
97
|
- img/logo.png
|
|
94
98
|
- img/rtfm-kb.png
|
|
95
99
|
- man/rtfm.1
|