rtfm-filemanager 4.10 → 5
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/README.md +278 -166
- data/bin/rtfm +2063 -1657
- data/img/logo.png +0 -0
- data/img/rtfm-kb.png +0 -0
- metadata +22 -14
data/bin/rtfm
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
-
#
|
3
|
-
|
4
|
-
# SCRIPT INFO
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# SCRIPT INFO {{{1
|
5
5
|
# Name: RTFM - Ruby Terminal File Manager
|
6
6
|
# Language: Pure Ruby, best viewed in VIM
|
7
7
|
# Author: Geir Isene <g@isene.com>
|
@@ -12,1326 +12,2007 @@
|
|
12
12
|
# this software for any purpose. I make no guarantee about the
|
13
13
|
# suitability of this software for any purpose and I am not liable
|
14
14
|
# for any damages resulting from its use. Further, I am under no
|
15
|
-
# obligation to maintain or extend this software. It is provided
|
15
|
+
# obligation to maintain or extend this software. It is provided
|
16
16
|
# on an 'as is' basis without any expressed or implied warranty.
|
17
|
-
|
18
|
-
|
19
|
-
#
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
17
|
+
# Docs: Apart from the extensive documentation found on Github, you can
|
18
|
+
# get a great understanding of the code itself by simply sending
|
19
|
+
# or pasting this whole file into you favorite AI for coding with
|
20
|
+
# a prompt like this: "Help me understand every part of this code".
|
21
|
+
@version = '5.0' # A full rewrite using https://github.com/isene/rcurses
|
22
|
+
|
23
|
+
# BOOTSNAP {{{1
|
24
|
+
cache_dir = ENV.fetch('BOOTSNAP_CACHE_DIR', File.join(Dir.home, '.rtfm', 'bootsnap-cache'))
|
25
|
+
ENV['BOOTSNAP_CACHE_DIR'] = cache_dir
|
26
|
+
require 'fileutils'
|
27
|
+
FileUtils.mkdir_p(cache_dir)
|
28
|
+
require 'bootsnap/setup' # Speed up subsequent requires
|
29
|
+
|
30
|
+
# ENCODING {{{1
|
31
|
+
# encoding: utf-8
|
32
|
+
|
33
|
+
# PROFILER {{{1
|
34
|
+
#$start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
35
|
+
#$last_checkpoint = $start_time
|
36
|
+
#$checkpoints = []
|
37
|
+
#def checkpoint(label)
|
38
|
+
# now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
39
|
+
# delta_ms = ((now - $last_checkpoint) * 1000).round(2)
|
40
|
+
# total_ms = ((now - $start_time) * 1000).round(2)
|
41
|
+
# $checkpoints << "#{label.ljust(20)}: #{delta_ms} ms"
|
42
|
+
# $checkpoints.reject! { |line| line.start_with?("Total:") }
|
43
|
+
# $checkpoints << "Total: #{total_ms} ms"
|
44
|
+
# $last_checkpoint = now
|
45
|
+
#end
|
46
|
+
|
47
|
+
# LOAD LIBRARIES {{{1
|
48
|
+
begin
|
49
|
+
require 'rcurses'
|
50
|
+
class Object
|
51
|
+
include Rcurses
|
52
|
+
include Rcurses::Input
|
53
|
+
end
|
54
|
+
rescue StandardError => e
|
55
|
+
puts 'RTFM is built using rcurses (https://github.com/isene/rcurses). Install rcurses to run RTFM.'
|
56
|
+
exit 1
|
57
|
+
end
|
58
|
+
require 'tmpdir'
|
59
|
+
# Lazy-load to speed up startup
|
60
|
+
autoload :Shellwords, 'shellwords'
|
61
|
+
autoload :Timeout, 'timeout'
|
62
|
+
autoload :Open3, 'open3'
|
63
|
+
autoload :PTY, 'pty'
|
64
|
+
autoload :OpenAI, 'ruby/openai'
|
65
|
+
#checkpoint("Libraries loaded")
|
66
|
+
|
67
|
+
# FIX TERMINAL MESSAGE BLEED-THROUGH {{{1
|
68
|
+
LOG_PATH = File.join(Dir.tmpdir, 'rtfm.log')
|
69
|
+
MAX_LOG_SIZE = 128 * 1024
|
70
|
+
if File.exist?(LOG_PATH) && File.size(LOG_PATH) > MAX_LOG_SIZE
|
71
|
+
# keep just the last MAX_LOG_SIZE bytes
|
72
|
+
File.open(LOG_PATH, 'r+b') do |f|
|
73
|
+
f.seek(-MAX_LOG_SIZE, IO::SEEK_END)
|
74
|
+
tail = f.read
|
75
|
+
f.rewind
|
76
|
+
f.truncate(0)
|
77
|
+
f.write(tail)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
logfile = File.open(LOG_PATH, 'a+')
|
81
|
+
logfile.sync = true
|
82
|
+
$stderr.reopen(logfile)
|
83
|
+
#checkpoint("Bleed-through fix")
|
84
|
+
|
85
|
+
# RCURSES CLASS EXTENSION {{{1
|
86
|
+
module Rcurses
|
87
|
+
# Add attributes, amend 'say' to set update to false
|
88
|
+
class Pane
|
89
|
+
attr_accessor :update, :locate
|
90
|
+
alias original_say say
|
91
|
+
def say(text)
|
92
|
+
original_say(text)
|
93
|
+
self.update = false
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# CREATE DIRS & SET FILE CONSTS {{{1
|
99
|
+
RTFM_HOME = File.join(Dir.home, '.rtfm')
|
100
|
+
PLUGINS_DIR = File.join(RTFM_HOME, 'plugins')
|
101
|
+
TRASH_DIR = File.join(RTFM_HOME, 'trash')
|
102
|
+
[RTFM_HOME, PLUGINS_DIR, TRASH_DIR].each { |d| FileUtils.mkdir_p(d) }
|
103
|
+
PREVIEW_FILE = File.join(RTFM_HOME, 'preview.rb')
|
104
|
+
KEYS_FILE = File.join(RTFM_HOME, 'keys.rb') unless defined?(KEYS_FILE)
|
105
|
+
CONFIG_FILE = File.join(RTFM_HOME, 'conf')
|
106
|
+
@plugin_errors = []
|
107
|
+
|
108
|
+
# SAVE TERMINAL {{{1
|
109
|
+
ORIG_STTY = `stty -g`.chomp
|
110
|
+
|
111
|
+
# HELP {{{1
|
112
|
+
@help = <<~HELPTEXT
|
113
|
+
RTFM - Ruby Terminal File Manager (https://github.com/isene/RTFM)
|
114
|
+
|
115
|
+
BASIC KEYS
|
116
|
+
? = Show this help text
|
117
|
+
v = Display RTFM version (and latest Gem version) in bottom window/command bar
|
118
|
+
r = Refresh RTFM (recreates all windows. Use on terminal resize or when there is garbage somewhere)
|
119
|
+
R = Reload configuration (~/.rtfm/conf)
|
120
|
+
W = Write parameters to ~/.rtfm/conf: @marks, @hash, @history, @rubyhistory, @aihistory
|
121
|
+
@lslong, @lsall, @lsorder, @lsinvert, @width, @border, @preview, @trash
|
122
|
+
C = Show the current configuration in ~/.rtfm/conf
|
123
|
+
q = Quit (save basic configuration: @marks, @hash, @history, @rubyhistory, @aihistory)
|
124
|
+
Q = QUIT (without writing any changes to the config file)
|
125
|
+
|
126
|
+
LAYOUT
|
127
|
+
w = Change the width of the left/right panes (left pane ⇒ 20%, 30%, 40%, 50%, 60%)
|
128
|
+
B = Cycle border
|
129
|
+
- = (Minus sign) Toggle preview in right pane (turn it off for faster traversing of directories)
|
130
|
+
_ = (Underscore) Toggle preview of images in right pane
|
131
|
+
b = Toggle syntax highlighting (and line numbering)
|
132
|
+
|
133
|
+
MOTION
|
134
|
+
j/DOWN = Go one item down in left pane (rounds to top)
|
135
|
+
k/UP = Go one item up in left pane (rounds to bottom)
|
136
|
+
h/LEFT = Go up one directory level
|
137
|
+
l/RIGHT = Enter directory or open file (using run-mailcap or xdg-open)
|
138
|
+
Use the key 'x' to force open using xdg-open (or run-mailcap) - used for opening html files
|
139
|
+
in a browser rather than editing the file in your text editor
|
140
|
+
PgDown = Go one page down in left pane
|
141
|
+
PgUp = Go one page up in left pane
|
142
|
+
END = Go to last item in left pane
|
143
|
+
HOME = Go to first item in left pane
|
144
|
+
|
145
|
+
MARKS AND JUMPING
|
146
|
+
m = Mark current dir (persistent). Next letter is the name of the mark [a-zA-Z']
|
147
|
+
The special mark "'" jumps to the last directory (makes toggling dirs easy)
|
148
|
+
Press '-' and a letter to delete that mark. Mark '0' is the dir where RTFM was started.
|
149
|
+
Marks '1' - '5' are the past five directories visited.
|
150
|
+
M = Show marked items in right pane
|
151
|
+
' = Jump to mark (next letter is the name of the mark [a-zA-Z'])
|
152
|
+
The 5 latest directories visited are stored in marks 1-5 (1 being the very latest)
|
153
|
+
~ = Jump to Home directory
|
154
|
+
> = Follow symlink to the directory where the target resides
|
155
|
+
|
156
|
+
DIRECTORY VIEWS
|
157
|
+
a = Show all (also hidden) items
|
158
|
+
A = Show long info per item (show item attributes)
|
159
|
+
o = Change the order/sorting of directories (circular toggle)
|
160
|
+
i = Invert/reverse the sorting
|
161
|
+
O = Show the Ordering in the bottom window (the full ls command)
|
162
|
+
|
163
|
+
TAGGING
|
164
|
+
t = Tag item (toggles)
|
165
|
+
Ctrl-t = Add items matching a pattern to list of tagged items (Ctrl-t and then . will tag all items)
|
166
|
+
T = Show currently tagged items in right pane
|
167
|
+
u = Untag all tagged items
|
168
|
+
|
169
|
+
MANIPULATE ITEMS
|
170
|
+
p = Put (copy) tagged items here
|
171
|
+
P = PUT (move) tagged items here
|
172
|
+
c = Change/rename selected (adds command to bottom window)
|
173
|
+
s = Create symlink to tagged items here
|
174
|
+
d = Delete selected item and tagged items. Confirm with 'y'.
|
175
|
+
Moves items to trash directory (~/.rtfm/trash/) if @trash = true
|
176
|
+
D = Empty trash directory
|
177
|
+
Ctrl-d = Toggle use of trash directory
|
178
|
+
Ctrl-o = Change ownership to user:group of selected and tagged items
|
179
|
+
Ctrl-p = Change permissions of selected and tagged items
|
180
|
+
Format = rwxr-xr-x or 755 or rwx (applies the trio to user, group and others)
|
181
|
+
|
182
|
+
FILTER AND SEARCH
|
183
|
+
f = Show only files in the left pane matching extension(s) (e.g. "txt" or "pdf,png,jpg")
|
184
|
+
F = Show only files matching a pattern (Ruby Regex) (e.g. "abc" or "ab.+12(\w3)+")
|
185
|
+
Ctrl-f = Clear all filtering
|
186
|
+
/ = Enter search string in bottom window to highlight matching items and jump to the first match
|
187
|
+
\\ = Remove search pattern
|
188
|
+
n = Jump to the next item matched by '/'
|
189
|
+
N = Jump to the previous item matched by '/'
|
190
|
+
g = Run 'grep' to show files that contains the MATCH in current directory
|
191
|
+
L = Start 'locate' search for files, then use '#' to jump to desired line/directory
|
192
|
+
Ctrl-l = Locate files via fzf from the current directory down
|
193
|
+
(fuzzy file finder must be installed https://github.com/junegunn/fzf)
|
194
|
+
|
195
|
+
ARCHIVES
|
196
|
+
z = Extract tagged zipped archive to current directory
|
197
|
+
Z = Create zipped archive from tagged files/directories
|
198
|
+
|
199
|
+
GIT/HASH/OPENAI
|
200
|
+
G = Show git status for current directory
|
201
|
+
H = Do a cryptographic hash of the current directory with subdirs
|
202
|
+
If a previous hash was made, compare and report if there has been any change
|
203
|
+
I = Show OpenAI's description of the selected item and its content (if available)
|
204
|
+
You must have installed the ruby-openai gem and added your openai secret key
|
205
|
+
in the ~/.rtfm/conf (add `@ai = "your-secret-openai-key") for this to work.
|
206
|
+
Ctrl-a = Start an OpenAI chat (the context window is kept until you exit RTFM)
|
207
|
+
The OpenAI agent is specialized in answering questions about cli, files and dirs
|
208
|
+
|
209
|
+
RIGHT PANE CONTROLS
|
210
|
+
ENTER = Refresh the right pane
|
211
|
+
S-RIGHT = One line down in the preview
|
212
|
+
S-LEFT = One line up in the preview
|
213
|
+
S-DOWN = Next page of the preview (if doc long and ∇ in the bottom right) (TAB does the same)
|
214
|
+
S-UP = Previous page (if you have moved down the document first - ∆ in the top right) (or S-TAB)
|
215
|
+
|
216
|
+
CLIPBOARD COPY
|
217
|
+
y = Copy path of selected item to primary selection (for pasting with middle mouse button)
|
218
|
+
Y = Copy path of selected item to clipboard
|
219
|
+
Ctrl-y = Copy content of right pane to clipboard
|
220
|
+
Turn off batcat syntax highlighting with 'b' for a clean copy of content
|
221
|
+
|
222
|
+
SYSTEM SHORTCUTS
|
223
|
+
S = Show comprehensive System info (system, CPU, filesystem, latest dmesg messages)
|
224
|
+
= = Create a new directory (a shortcut for ":mkdir ")
|
225
|
+
Ctrl-n = Invoke navi (see https://github.com/denisidoro/navi) with any output in right window
|
226
|
+
|
227
|
+
COMMAND MODE
|
228
|
+
: = Enter "command mode" in bottom window (press ENTER to execute, press ESC to escape)
|
229
|
+
Prefix the command with a '§' to force the program to run in interactive mode
|
230
|
+
Full screen TUI programs like htop, vim or any shell must run in interactive mode
|
231
|
+
; = Show command history in right pane
|
232
|
+
+ = Add program(s) to the list of full-UI interactive terminal programs
|
233
|
+
|
234
|
+
RUBY DEBUG MODE
|
235
|
+
@ = Enter Ruby mode to execute any Ruby command (ENTER to execute, ESC to escape)
|
236
|
+
|
237
|
+
COPYRIGHT: Geir Isene, 2025+. No rights reserved. See http://isene.com for more.
|
119
238
|
HELPTEXT
|
120
|
-
def firstrun
|
121
|
-
@firstrun = <<~FIRSTRUN
|
122
239
|
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
240
|
+
# FIRST RUN # {{{1
|
241
|
+
def firstrun # {{{2
|
242
|
+
setup_launch_script
|
243
|
+
setup_config
|
244
|
+
load_config
|
245
|
+
setup_templates
|
246
|
+
display_welcome_message
|
247
|
+
getchr
|
248
|
+
rescue StandardError => e
|
249
|
+
errormsg('⚠ Errors during RTFM first-run.', e)
|
250
|
+
exit 1
|
251
|
+
end
|
252
|
+
|
253
|
+
def setup_launch_script # {{{2
|
254
|
+
shell_name = File.basename(ENV['SHELL'].to_s)
|
255
|
+
if shell_name == 'fish'
|
256
|
+
fish_cfg = File.join(Dir.home, '.config', 'fish', 'config.fish')
|
257
|
+
FileUtils.mkdir_p(File.dirname(fish_cfg))
|
258
|
+
line = 'function r; rtfm; cd (cat ~/.rtfm_last_dir); end # rtfm launcher'
|
259
|
+
File.write(fish_cfg, "#{line}\n", mode: 'a')
|
260
|
+
else
|
261
|
+
rc_file = File.join(Dir.home, ".#{shell_name}rc")
|
262
|
+
FileUtils.mkdir_p(File.dirname(rc_file))
|
263
|
+
line = 'r(){ rtfm; cd "$(cat ~/.rtfm_last_dir)"; } # rtfm launcher'
|
264
|
+
File.write(rc_file, "#{line}\n", mode: 'a')
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
def setup_config # {{{2
|
269
|
+
File.write CONFIG_FILE, <<~CONFIG
|
270
|
+
# Configuration file for RTFM (https://github.com/isene/RTFM)
|
271
|
+
# BASICS
|
272
|
+
@width = 2
|
273
|
+
@border = 1
|
274
|
+
@trash = true
|
275
|
+
|
276
|
+
# LS
|
277
|
+
@lslong = ""
|
278
|
+
@lsall = ""
|
279
|
+
@lsorder = ""
|
280
|
+
@lsinvert = ""
|
281
|
+
|
282
|
+
# RIGHT PANE
|
283
|
+
@runmailcap = true
|
284
|
+
@preview = true
|
285
|
+
@showimage = true
|
286
|
+
|
287
|
+
# TOP PANE
|
288
|
+
@topmatch = [["", 249]]
|
289
|
+
|
290
|
+
# INTERACTIVE
|
291
|
+
@interactive = "fzf,navi,top,htop,less,vi,vim,ncdu,sh,zsh,bash,fish"
|
292
|
+
|
293
|
+
# AI
|
294
|
+
@ai = "Replace_This_With_Your_OpenAI_Key"
|
295
|
+
@aimodel = "gpt-4o-mini"
|
296
|
+
|
297
|
+
# LISTS
|
298
|
+
@marks = {}
|
299
|
+
@hash = {}
|
300
|
+
@history = []
|
301
|
+
@rubyhistory = []
|
302
|
+
@aihistory = []
|
303
|
+
CONFIG
|
304
|
+
end
|
305
|
+
|
306
|
+
def setup_templates # {{{2
|
307
|
+
unless File.exist?(PREVIEW_FILE) # {{{3
|
308
|
+
File.write PREVIEW_FILE, <<~TEMPLATE
|
309
|
+
# ~/.rtfm/preview.rb
|
310
|
+
#
|
311
|
+
# Define one handler per line in the form:
|
312
|
+
#
|
313
|
+
# ext1, ext2, ext3 = command with @s placeholder
|
314
|
+
#
|
315
|
+
# @s will be replaced by the shell-escaped filename.
|
316
|
+
#
|
317
|
+
# Lines beginning with # or blank are ignored.
|
318
|
+
#
|
319
|
+
# Examples:
|
320
|
+
# # plain text, Ruby, Python, shell
|
321
|
+
# txt, rb, py, sh = bat -n --color=always @s
|
322
|
+
#
|
323
|
+
# # markdown via pandoc
|
324
|
+
# md = pandoc @s -t plain
|
325
|
+
#
|
326
|
+
# # PDFs
|
327
|
+
# pdf = pdftotext -f 1 -l 4 @s -
|
328
|
+
#
|
329
|
+
TEMPLATE
|
330
|
+
end
|
331
|
+
|
332
|
+
unless File.exist?(KEYS_FILE) # {{{3
|
333
|
+
File.write KEYS_FILE, <<~TEMPLATE
|
334
|
+
# ~/.rtfm/keys.rb
|
335
|
+
#
|
336
|
+
# Override or add key bindings simply by assigning into KEYMAP
|
337
|
+
# and defining the corresponding handler methods.
|
338
|
+
#
|
339
|
+
# Syntax:
|
340
|
+
# KEYMAP['X'] = :my_handler
|
341
|
+
#
|
342
|
+
# def my_handler(chr)
|
343
|
+
# # anything you like—use @pB, @pR, Dir.pwd, etc.
|
344
|
+
# @pB.say("You pressed X!")
|
345
|
+
# end
|
346
|
+
#
|
347
|
+
# Examples:
|
348
|
+
#
|
349
|
+
# # remap 'C' to show config
|
350
|
+
# KEYMAP['C'] = :show_config
|
351
|
+
#
|
352
|
+
# # add a new binding: 'Z'
|
353
|
+
# KEYMAP['Z'] = :zap_all
|
354
|
+
# def zap_all(_chr)
|
355
|
+
# @pB.say("ZAPPED!")
|
356
|
+
# end
|
357
|
+
TEMPLATE
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
def display_welcome_message # {{{2
|
362
|
+
# rubocop:disable Layout/IndentationWidth
|
363
|
+
@firstrun = "
|
364
|
+
Welcome to RTFM - the Ruby Terminal File Manager. This help text is shown on the first run.
|
365
|
+
Next time you run RTFM, you can launch it from your terminal with a one letter command: 'r'
|
366
|
+
When launched this way, RTFM will also exit in the directory you are currently in.\n".b.fg(226)
|
367
|
+
@firstrun += "
|
368
|
+
To benefit fully from all the features, you need to install some auxiliary software.
|
369
|
+
On Ubuntu Linux you would use 'apt install':\n".fg(230)
|
370
|
+
@firstrun += "
|
371
|
+
Basic requirements is 'x11-utils' and 'xdotool': apt install x11-utils xdotool
|
372
|
+
|
373
|
+
Viewers (you may install more sepcialised viewers later:
|
374
|
+
Syntax highlighting of text uses 'bat': apt install bat
|
375
|
+
Viewing Markdown uses 'pandoc': apt install pandoc
|
376
|
+
Viewing PDFs uses 'pdftotext': apt install poppler-utils
|
377
|
+
Viewing LibreOffice docs uses 'odt2txt': apt install odt2txt
|
378
|
+
Viewing MS docx uses 'docx2txt': apt install docx2txt
|
379
|
+
Viewing MS pptx uses 'unzip': apt install unzip
|
380
|
+
Viewing MS xlsx uses 'ssconvert': apt install gnumeric
|
381
|
+
Viewing MS doc/xls/ppt uses 'catdoc, xls2csv and catppt': apt install catdoc
|
382
|
+
Viewing Images uses 'w3m and ImageMagick': apt install w3m imagemagick
|
383
|
+
Viewing Video thumbnails uses 'ffmpegthumbnailer' apt install ffmpegthumbnailer\n".fg(195)
|
384
|
+
@firstrun += "
|
385
|
+
All of the above in one fell swoop:\n".fg(159)
|
386
|
+
@firstrun += "
|
387
|
+
sudo apt install ruby-full git libncurses-dev x11-utils xdotool bat pandoc poppler-utils odt2txt docx2txt unzip gnumeric catdoc w3m imagemagick ffmpegthumbnailer".b.fg(87)
|
388
|
+
@firstrun += "\n\n
|
389
|
+
You need to install the basic requirements and you should install the viewers before you...
|
390
|
+
... hit any key to start RTFM. Hit ? inside RTFM to show the full help text. Enjoy :-)\n".fg(214)
|
391
|
+
# rubocop:enable Layout/IndentationWidth
|
392
|
+
puts @firstrun
|
393
|
+
end
|
394
|
+
#checkpoint("Preliminaries done")
|
395
|
+
|
396
|
+
# BASIC SETUP {{{1
|
397
|
+
## Check for installed basic applications {{{2
|
398
|
+
def cmd?(cmd) # Helper function
|
399
|
+
system("which #{cmd} > /dev/null 2>&1")
|
400
|
+
end
|
401
|
+
|
402
|
+
if cmd?('/usr/lib/w3m/w3mimgdisplay')
|
403
|
+
@imgdisplay = '/usr/lib/w3m/w3mimgdisplay'
|
404
|
+
@showimage = true
|
405
|
+
else
|
406
|
+
@showimage = false
|
407
|
+
end
|
408
|
+
@showimage = false unless cmd?('xwininfo') && cmd?('xdotool')
|
409
|
+
|
410
|
+
@bat = cmd?('bat') ? 'bat' : 'batcat'
|
411
|
+
|
412
|
+
## Set encoding for $stdin to utf-8 {{{2
|
413
|
+
$stdin.set_encoding(Encoding::UTF_8)
|
414
|
+
|
415
|
+
# INITIALIZE VARIABLES {{{1
|
416
|
+
## These can be set by user in ~/.rtfm/conf
|
417
|
+
### Saved on quit ('q')
|
418
|
+
@history = [] # Initialize the command line history array
|
419
|
+
@rubyhistory = [] # Initialize the command line history array for ruby commands
|
420
|
+
@aihistory = [] # Initialize the command line history array for AI chat
|
421
|
+
### Saved on Write Config ('W')
|
422
|
+
@lslong = '' # Set short form ls (toggled by pressing "A")
|
423
|
+
@lsall = '' # Set "ls -a" to false (toggled by pressing "a" - sets it to "-a")
|
424
|
+
@lsorder = '' # Change the order/sorting by pressing 'o' (circular toggle)
|
425
|
+
@lsinvert = '' # Set to "-r" to reverse/invert sorting order
|
426
|
+
@width = 4 # Set width of the left pane to the default ⅓ of the terminal width
|
427
|
+
@border = 1 # Set initial border config; 0 = none, 1 = Right pane, 2 = both panes, 3 = Left pane
|
428
|
+
@preview = true # Preview in the Right pane
|
429
|
+
@trash = false # Delete files permanently rather than moving them to ~/.rtfm/trash
|
430
|
+
@interactive = 'fzf,navi,top,htop,less,vi,vim,ncdu,sh,zsh,bash,fish'
|
431
|
+
### Not saved (but can be set in ~/.rtfm/conf)
|
432
|
+
@topcolor = 249 # Default Top pane bg color
|
433
|
+
@topmatch = [['', 249]] # Default Top pane bg color for matching directories
|
434
|
+
@bottomcolor = 236 # Default Bottom pane color
|
435
|
+
@searchcolor = 23 # Default color for Search pane at bottom
|
436
|
+
@cmdcolor = 18 # Default color for Command pane at bottom
|
437
|
+
@rubycolor = 52 # Default color for Ruby pane at bottom
|
438
|
+
@aicolor = 58
|
439
|
+
@lsbase = '--group-directories-first' # Basic ls setup
|
440
|
+
@lsuser = '' # Set this variable in ~/.rtfm/conf to any 'ls' switch you want to customize directory listings
|
441
|
+
@batuse = true # Use batcat for syntax highlighting
|
442
|
+
@runmailcap = false # Set to 'true' in ~/.rtfm/conf if you want to use run-mailcap instead of xdg-open
|
443
|
+
@aimodel = 'gpt-4o-mini' # The default OpenAI model, set another in ~/.rtfm/conf if you like
|
444
|
+
|
445
|
+
## These are automatically written to ~/.rtfm/conf upon exit
|
446
|
+
@marks = {} # Initialize (book)marks hash
|
447
|
+
@hash = {} # Initialize the sha directory hashing
|
448
|
+
|
449
|
+
## Do not set these in ~/.rtfm/conf
|
450
|
+
@tagged = [] # Initialize the tagged array - for collecting all tagged items
|
451
|
+
@directory = {} # Initialize the directory hash for remembering directories visited
|
452
|
+
@searched = '' # Initialize the active searched for items
|
453
|
+
@lsfiles = '' # File types to show (initially set to all file types) - not saved on exit
|
454
|
+
@lsmatch = '' # Files to match (initially set to matching all files) - not saved on exit
|
455
|
+
@index = 0 # Set chosen item to first on startup
|
456
|
+
@tagsize = 0 # Size (in MB) of tagged items
|
457
|
+
@navi = '' # Navi result when navi is invoked
|
458
|
+
|
459
|
+
# LOAD CONFIG {{{1
|
460
|
+
## Get variables from config file (written back to ~/.rtfm/conf when exit via 'q')
|
461
|
+
def load_config
|
462
|
+
if File.exist?(CONFIG_FILE)
|
463
|
+
load(CONFIG_FILE)
|
464
|
+
@conf = File.read(CONFIG_FILE).dup
|
465
|
+
else
|
466
|
+
firstrun
|
467
|
+
end
|
468
|
+
rescue StandardError => e
|
469
|
+
errormsg("⚠ Errors while loading #{CONFIG_FILE}\nCheck your config file or delete it to remake in a fresh RTFM start.", e)
|
470
|
+
end
|
471
|
+
load_config
|
472
|
+
@marks['0'] = Dir.pwd # Original dir
|
473
|
+
@marks["'"] = Dir.pwd # Initial mark
|
474
|
+
#checkpoint("Vars/config loaded")
|
475
|
+
|
476
|
+
# Handle start dir {{{2
|
477
|
+
Dir.chdir(ARGV.shift) if ARGV[0] && File.directory?(ARGV[0])
|
478
|
+
|
479
|
+
# OPENAI SETUP {{{1
|
480
|
+
def chat_history # {{{2
|
481
|
+
@chat_history ||= [
|
482
|
+
{ role: 'system',
|
483
|
+
content: 'You are a helpful assistant embedded in a terminal file manager. ' \
|
484
|
+
'Answer questions about files, directories, or shell commands.' }
|
485
|
+
]
|
486
|
+
end
|
487
|
+
|
488
|
+
def openai_client # {{{2
|
489
|
+
require 'ruby/openai' unless defined?(OpenAI)
|
490
|
+
@openai_client ||= OpenAI::Client.new(access_token: @ai)
|
491
|
+
end
|
492
|
+
|
493
|
+
# SET UP VIEWER SYSTEM {{{1
|
494
|
+
# rubocop:disable Style/StringLiterals
|
495
|
+
preview_specs = {
|
496
|
+
'txt, rb, py, sh' => "#{@bat} -n --color=always @s",
|
497
|
+
'md' => "pandoc @s -t plain",
|
498
|
+
'pdf' => "pdftotext -f 1 -l 4 @s -",
|
499
|
+
'odt, odp, odg, odc' => "odt2txt @s",
|
500
|
+
'docx' => "docx2txt @s -",
|
501
|
+
'xlsx' => "ssconvert -O 'separator= ' -T Gnumeric_stf:stf_assistant @s fd://1",
|
502
|
+
'pptx' => "unzip -qc @s | ruby -e '$stdin.each_line{ |i| i.force_encoding(\"ISO-8859-1\").scan(/<a:t>(.+?)<\\/a:t>/).each{ |j| puts j } }'",
|
503
|
+
'doc' => "catdoc @s 2>/dev/null",
|
504
|
+
'xls' => "xls2csv @s 2>/dev/null",
|
505
|
+
'ppt' => "catppt @s 2>/dev/null",
|
506
|
+
# images ⇒ nil => call showimage
|
507
|
+
'png, jpg, jpeg, bmp, gif, webp, tif, tiff' => nil,
|
508
|
+
# video ⇒ nil, but detected via pattern below
|
509
|
+
'mpg, mpeg, avi, mov, mkv, mp4' => nil
|
510
|
+
}
|
511
|
+
@imagefile ||= /\.(?:png|jpe?g|bmp|gif|webp|tiff?)$/i
|
512
|
+
@pdffile ||= /\.pdf$/i
|
513
|
+
# rubocop:enable Style/StringLiterals
|
514
|
+
|
515
|
+
# USER PLUGINS {{{1
|
516
|
+
# Merge in any user overrides from ~/.rtfm/preview.rb
|
517
|
+
if File.exist?(PREVIEW_FILE)
|
518
|
+
begin
|
519
|
+
File.readlines(PREVIEW_FILE).each_with_index do |line, idx|
|
520
|
+
line = line.strip
|
521
|
+
next if line.empty? || line.start_with?('#')
|
522
|
+
|
523
|
+
if line =~ /\A([0-9A-Za-z_,\s]+)\s*=\s*(.+)\z/
|
524
|
+
exts_str = $1.strip
|
525
|
+
cmd_str = $2.strip
|
526
|
+
preview_specs[exts_str] = cmd_str
|
527
|
+
else
|
528
|
+
@plugin_errors << "Invalid preview.rb line #{idx + 1}: #{line}"
|
529
|
+
end
|
530
|
+
end
|
531
|
+
rescue => e
|
532
|
+
@plugin_errors << "Error loading preview.rb: #{e.class}: #{e.message}"
|
533
|
+
end
|
534
|
+
end
|
535
|
+
# Compile into an array of [Regexp, template] for fast lookup
|
536
|
+
PREVIEW_HANDLERS = preview_specs.map do |exts_str, tmpl|
|
537
|
+
exts = exts_str.split(',').map(&:strip).map { |ext| Regexp.escape(ext) }.join('|')
|
538
|
+
[/\.#{exts}$/i, tmpl]
|
539
|
+
end
|
540
|
+
#checkpoint("Plugins loaded")
|
541
|
+
|
542
|
+
# KEY DISPATCH TABLE & HANDLERS {{{1
|
543
|
+
KEYMAP = { # {{{2
|
544
|
+
# BASIC KEYS {{{3
|
545
|
+
'?' => :show_help,
|
546
|
+
'v' => :show_version,
|
547
|
+
'r' => :refresh_all,
|
548
|
+
'R' => :load_config,
|
549
|
+
'C' => :show_config,
|
550
|
+
'W' => :write_config,
|
551
|
+
'q' => :quit_and_save,
|
552
|
+
'Q' => :quit_no_save,
|
553
|
+
|
554
|
+
# LAYOUT {{{3
|
555
|
+
'w' => :change_width,
|
556
|
+
'B' => :toggle_border,
|
557
|
+
'-' => :toggle_preview,
|
558
|
+
'_' => :toggle_image,
|
559
|
+
'b' => :toggle_syntax,
|
560
|
+
|
561
|
+
# MOTION {{{3
|
562
|
+
'DOWN' => :move_down,
|
563
|
+
'j' => :move_down,
|
564
|
+
'C-DOWN' => :move_down,
|
565
|
+
'UP' => :move_up,
|
566
|
+
'k' => :move_up,
|
567
|
+
'C-UP' => :move_up,
|
568
|
+
'LEFT' => :move_left,
|
569
|
+
'h' => :move_left,
|
570
|
+
'C-LEFT' => :move_left,
|
571
|
+
'RIGHT' => :move_right,
|
572
|
+
'l' => :move_right,
|
573
|
+
'C-RIGHT' => :move_right,
|
574
|
+
'x' => :open_force,
|
575
|
+
'PgDOWN' => :page_down,
|
576
|
+
'PgUP' => :page_up,
|
577
|
+
'END' => :go_last,
|
578
|
+
'HOME' => :go_first,
|
579
|
+
|
580
|
+
# MARKS & JUMPING {{{3
|
581
|
+
'm' => :set_mark,
|
582
|
+
'M' => :show_marks,
|
583
|
+
"'" => :jump_to_mark,
|
584
|
+
'~' => :go_home,
|
585
|
+
'>' => :follow_symlink,
|
586
|
+
|
587
|
+
# DIRECTORY VIEWS {{{3
|
588
|
+
'a' => :toggle_all,
|
589
|
+
'A' => :toggle_long,
|
590
|
+
'o' => :toggle_order,
|
591
|
+
'i' => :toggle_invert,
|
592
|
+
'O' => :show_ls_command,
|
593
|
+
|
594
|
+
# TAGGING {{{3
|
595
|
+
't' => :tag_current,
|
596
|
+
'C-T' => :tag_pattern,
|
597
|
+
'T' => :show_tagged,
|
598
|
+
'u' => :clear_tagged,
|
599
|
+
|
600
|
+
# MANIPULATE ITEMS {{{3
|
601
|
+
'p' => :copy_items,
|
602
|
+
'P' => :move_items,
|
603
|
+
'c' => :rename_item,
|
604
|
+
's' => :link_items,
|
605
|
+
'd' => :delete_items,
|
606
|
+
'D' => :empty_trash,
|
607
|
+
'C-D' => :toggle_trash,
|
608
|
+
'C-O' => :change_ownership,
|
609
|
+
'C-P' => :change_permissions,
|
610
|
+
|
611
|
+
# FILTER AND SEARCH {{{3
|
612
|
+
'f' => :filter_types,
|
613
|
+
'F' => :filter_regex,
|
614
|
+
'C-F' => :filter_clear,
|
615
|
+
'/' => :search_text,
|
616
|
+
'\\' => :clear_search,
|
617
|
+
'n' => :search_next,
|
618
|
+
'N' => :search_prev,
|
619
|
+
'g' => :grep_current,
|
620
|
+
'L' => :locate,
|
621
|
+
'#' => :jump_locate,
|
622
|
+
'C-L' => :fzf_jump,
|
623
|
+
|
624
|
+
# ARCHIVES {{{3
|
625
|
+
'z' => :unzip_items,
|
626
|
+
'Z' => :zip_items,
|
627
|
+
|
628
|
+
# GIT/HASH/OPENAI {{{3
|
629
|
+
'G' => :git_status,
|
630
|
+
'H' => :hash_directory,
|
631
|
+
'I' => :openai_description,
|
632
|
+
'C-A' => :chat_mode,
|
633
|
+
|
634
|
+
# RIGHT PANE CONTROLS {{{3
|
635
|
+
'ENTER' => :refresh_right,
|
636
|
+
'S-DOWN' => :line_down_right,
|
637
|
+
'S-UP' => :line_up_right,
|
638
|
+
'S-RIGHT' => :page_down_right,
|
639
|
+
'TAB' => :page_down_right,
|
640
|
+
'S-LEFT' => :page_up_right,
|
641
|
+
'S-TAB' => :page_up_right,
|
642
|
+
|
643
|
+
# CLIPBOARD COPY {{{3
|
644
|
+
'y' => :copy_path,
|
645
|
+
'Y' => :copy_path,
|
646
|
+
'C-Y' => :copy_right,
|
647
|
+
|
648
|
+
# SYSTEM SHORTCUTS {{{3
|
649
|
+
'S' => :system_info,
|
650
|
+
'=' => :make_directory,
|
651
|
+
'C-N' => :navi_invoke,
|
652
|
+
|
653
|
+
# COMMAND MODE {{{3
|
654
|
+
':' => :command_mode,
|
655
|
+
';' => :show_history,
|
656
|
+
'+' => :add_interactive,
|
657
|
+
|
658
|
+
# RUBY MODE {{{3
|
659
|
+
'@' => :ruby_debug
|
660
|
+
}
|
661
|
+
|
662
|
+
# USER KEYS (override or extend KEYMAP) {{{2
|
663
|
+
if File.exist?(KEYS_FILE)
|
664
|
+
begin
|
665
|
+
load KEYS_FILE
|
666
|
+
rescue => e
|
667
|
+
@plugin_errors << "Error loading keys.rb: #{e.class}: #{e.message}"
|
668
|
+
end
|
669
|
+
end
|
670
|
+
|
671
|
+
# MAIN GETKEY FOR USER INPUT {{{2
|
672
|
+
def getkey # {{{3
|
673
|
+
chr = getchr(1)
|
674
|
+
return unless chr
|
675
|
+
|
676
|
+
showimage('clear') if @image
|
677
|
+
@image = false
|
678
|
+
if handler = KEYMAP[chr] # rubocop:disable Lint/AssignmentInCondition
|
679
|
+
m = method(handler)
|
680
|
+
m.arity == 1 ? m.call(chr) : m.call
|
681
|
+
end
|
682
|
+
rescue StandardError => e
|
683
|
+
errormsg('⚷ Error in getkey', e)
|
684
|
+
end
|
685
|
+
|
686
|
+
# BASIC KEYS {{{2
|
687
|
+
def show_help # {{{3
|
688
|
+
@pR.say(@help.fg(249))
|
689
|
+
end
|
690
|
+
|
691
|
+
def show_version # {{{3
|
692
|
+
@pB.say(" RTFM version = #{@version} (latest RubyGems version is #{Gem.latest_version_for('rtfm-filemanager').version} - https://github.com/isene/RTFM)".b)
|
693
|
+
end
|
694
|
+
|
695
|
+
def refresh_all # {{{3
|
696
|
+
refresh
|
697
|
+
end
|
698
|
+
|
699
|
+
def show_config
|
700
|
+
@pR.say('Configuration'.u.fg(254) + ":\n\n" + @conf.fg(249))
|
701
|
+
end
|
702
|
+
|
703
|
+
def write_config # {{{3
|
704
|
+
conf_write(all: true)
|
705
|
+
show_config
|
706
|
+
end
|
707
|
+
|
708
|
+
def quit_and_save # {{{3
|
709
|
+
shell("printf \"\e]0;#{Dir.pwd}\007\"")
|
710
|
+
conf_write
|
711
|
+
exit_rtfm
|
712
|
+
end
|
713
|
+
|
714
|
+
def quit_no_save # {{{3
|
715
|
+
shell("printf \"\e]0;#{Dir.pwd}\007\"")
|
716
|
+
exit_rtfm
|
717
|
+
end
|
718
|
+
|
719
|
+
# LAYOUT {{{2
|
720
|
+
def change_width # {{{3
|
721
|
+
@width += 1
|
722
|
+
@width = 2 if @width == 8
|
723
|
+
refresh
|
724
|
+
@pR.update = @pB.update = true
|
725
|
+
end
|
726
|
+
|
727
|
+
def toggle_border # {{{3
|
728
|
+
@border = (@border + 1) % 4
|
729
|
+
setborder
|
730
|
+
end
|
731
|
+
|
732
|
+
def toggle_preview # {{{3
|
733
|
+
@preview = !@preview
|
734
|
+
@pB.say("Preview = #{@preview ? 'On' : 'Off'}")
|
735
|
+
@pR.clear unless @preview
|
736
|
+
end
|
737
|
+
|
738
|
+
def toggle_image # {{{3
|
739
|
+
@showimage = !@showimage
|
740
|
+
@pB.say("Image preview = #{@showimage ? 'On' : 'Off'}")
|
741
|
+
getch
|
742
|
+
end
|
743
|
+
|
744
|
+
def toggle_syntax # {{{3
|
745
|
+
@batuse = !@batuse
|
746
|
+
@pB.say("Syntax highlighting = #{@batuse ? 'On' : 'Off'}")
|
747
|
+
@pR.update = true
|
748
|
+
end
|
749
|
+
|
750
|
+
# MOTION {{{2
|
751
|
+
def move_down # {{{3
|
752
|
+
@index = @index >= @max_index ? @min_index : @index + 1
|
753
|
+
@pR.update = @pB.update = true
|
754
|
+
end
|
755
|
+
|
756
|
+
def move_up # {{{3
|
757
|
+
@index = @index <= @min_index ? @max_index : @index - 1
|
758
|
+
@pR.update = @pB.update = true
|
759
|
+
end
|
760
|
+
|
761
|
+
def move_left # {{{3
|
762
|
+
old_dir = Dir.pwd
|
763
|
+
parent = File.dirname(old_dir)
|
764
|
+
child = File.basename(old_dir)
|
765
|
+
purels = command(
|
766
|
+
"ls #{Shellwords.escape(parent)} #{@lsbase} #{@lsall} #{@lsorder} #{@lsinvert} #{@lsuser}"
|
767
|
+
).pure.split("\n")
|
768
|
+
child_idx = purels.index(child) || 0
|
769
|
+
@directory[parent] = child_idx
|
770
|
+
mark_latest
|
771
|
+
Dir.chdir(parent)
|
772
|
+
@pL.update = @pR.update = @pB.update = true
|
773
|
+
end
|
774
|
+
|
775
|
+
def move_right # {{{3
|
776
|
+
@directory[Dir.pwd] = @index
|
777
|
+
mark_latest
|
778
|
+
open_selected
|
779
|
+
@index = @directory[Dir.pwd] || 0
|
780
|
+
@pB.update = true
|
781
|
+
end
|
782
|
+
|
783
|
+
def open_force # {{{3
|
784
|
+
@directory[Dir.pwd] = @index
|
785
|
+
mark_latest
|
786
|
+
open_selected(true)
|
787
|
+
@pB.update = true
|
788
|
+
end
|
789
|
+
|
790
|
+
def page_down # {{{3
|
791
|
+
@index += @pL.h - 2
|
792
|
+
@index = @max_index if @index > @max_index
|
793
|
+
@pR.update = @pB.update = true
|
794
|
+
end
|
795
|
+
|
796
|
+
def page_up # {{{3
|
797
|
+
@index -= @pL.h - 2
|
798
|
+
@index = @min_index if @index < @min_index
|
799
|
+
@pR.update = @pB.update = true
|
800
|
+
end
|
801
|
+
|
802
|
+
def go_last # {{{3
|
803
|
+
@index = @max_index
|
804
|
+
@pR.update = @pB.update = true
|
805
|
+
end
|
806
|
+
|
807
|
+
def go_first # {{{3
|
808
|
+
@index = @min_index
|
809
|
+
@pR.update = @pB.update = true
|
810
|
+
end
|
811
|
+
|
812
|
+
# MARKS & JUMPING {{{2
|
813
|
+
def set_mark # {{{3
|
814
|
+
marks_info
|
815
|
+
@pB.say("Set mark by pressing any letter. Delete mark by pressing '-' and the letter of the mark to remove.".fg(156))
|
816
|
+
m = getchr
|
817
|
+
return if m == 'ESC'
|
818
|
+
|
819
|
+
if m =~ /[\w']/
|
820
|
+
@marks[m] = Dir.pwd
|
821
|
+
elsif m == '-'
|
822
|
+
@pB.say('Press the letter of the mark to remove.'.fg(156))
|
823
|
+
r = getchr; @marks.delete(r)
|
824
|
+
end
|
825
|
+
marks_info
|
826
|
+
@pB.update = true
|
827
|
+
end
|
828
|
+
|
829
|
+
def show_marks # {{{3
|
830
|
+
marks_info
|
831
|
+
@pB.update = true
|
832
|
+
end
|
833
|
+
|
834
|
+
def jump_to_mark # {{{3
|
835
|
+
marks_info
|
836
|
+
m = getchr
|
837
|
+
if m =~ /[\w']/ && @marks[m]
|
838
|
+
@directory[Dir.pwd] = @index
|
839
|
+
dir_before = Dir.pwd
|
840
|
+
begin; Dir.chdir(@marks[m]); rescue; @pB.say(' No such directory'); end
|
841
|
+
mark_latest
|
842
|
+
@marks["'"] = dir_before
|
843
|
+
end
|
844
|
+
@pR.update = @pB.update = true
|
845
|
+
end
|
846
|
+
|
847
|
+
def go_home # {{{3
|
848
|
+
@directory[Dir.pwd] = @index
|
849
|
+
mark_latest
|
850
|
+
Dir.chdir
|
851
|
+
@pR.update = @pB.update = true
|
852
|
+
end
|
853
|
+
|
854
|
+
def follow_symlink # {{{3
|
855
|
+
@directory[Dir.pwd] = @index; mark_latest
|
856
|
+
if File.symlink?(@selected)
|
857
|
+
begin
|
858
|
+
target = File.readlink(@selected)
|
859
|
+
target = File.expand_path(target, File.dirname(@selected)) unless target.start_with?('/')
|
860
|
+
Dir.chdir(target)
|
861
|
+
rescue => e
|
862
|
+
@pB.say("Error following symlink: #{e}")
|
863
|
+
end
|
864
|
+
end
|
865
|
+
@pB.update = true
|
866
|
+
end
|
867
|
+
|
868
|
+
# DIRECTORY VIEWS {{{2
|
869
|
+
def toggle_all # {{{3
|
870
|
+
@lsall = @lsall.empty? ? '-a' : ''
|
871
|
+
@pR.update = @pB.update = true
|
872
|
+
end
|
873
|
+
|
874
|
+
def toggle_long # {{{3
|
875
|
+
@lslong = @lslong.empty? ? '-lh --time-style=long-iso' : ''
|
876
|
+
@pR.update = @pB.update = true
|
877
|
+
end
|
878
|
+
|
879
|
+
def toggle_order # {{{3
|
880
|
+
case @lsorder
|
881
|
+
when ''
|
882
|
+
@lsorder = '-S'; @pB.say(' Sorting by size')
|
883
|
+
when '-S'
|
884
|
+
@lsorder = '-t'; @pB.say(' Sorting by time')
|
885
|
+
when '-t'
|
886
|
+
@lsorder = '-X'; @pB.say(' Sorting by extension')
|
887
|
+
else
|
888
|
+
@lsorder = ''; @pB.say(' Normal sorting')
|
889
|
+
end
|
890
|
+
@pR.update = true; @orderchange = true
|
891
|
+
end
|
892
|
+
|
893
|
+
def toggle_invert # {{{3
|
894
|
+
@lsinvert = @lsinvert.empty? ? '-r' : ''
|
895
|
+
@pB.say(' Sorting inverted')
|
896
|
+
@pR.update = true; @orderchange = true
|
897
|
+
end
|
898
|
+
|
899
|
+
def show_ls_command # {{{3
|
900
|
+
@pB.say(" Full 'ls' command: ls #{@lsbase} #{@lslong} #{@lsall} #{@lsorder} #{@lsinvert} #{@lsuser}".gsub(/ +/, ' '))
|
901
|
+
@pB.update = false
|
902
|
+
end
|
903
|
+
|
904
|
+
# TAGGING {{{2
|
905
|
+
def tag_current # {{{3
|
906
|
+
item = @selected
|
907
|
+
if @tagged.include?(item)
|
908
|
+
@tagged.delete(item); @tagsize -= File.size(item) rescue 0
|
909
|
+
else
|
910
|
+
@tagged.push(item); @tagsize += File.size(item) rescue 0
|
911
|
+
end
|
912
|
+
@index = [@index + 1, (@files.size - 1)].min
|
913
|
+
@pB.say(" Tagged #{@tagged.size} files (#{(@tagsize.to_f / 1_000_000).round(2)}MB)".fg(204))
|
914
|
+
@pB.update = false; @pR.update = true; @pL.update = true
|
915
|
+
end
|
916
|
+
|
917
|
+
def tag_pattern # {{{3
|
918
|
+
pat = @pB.ask('Tag pattern (ruby regex): ', '')
|
919
|
+
re = Regexp.new(pat)
|
920
|
+
matches = @files.grep(re).map { |t| File.join(Dir.pwd, t) }
|
921
|
+
matches.each do |f|
|
922
|
+
@tagsize += File.size(f) rescue nil
|
923
|
+
end
|
924
|
+
@tagged.concat(matches)
|
925
|
+
@tagged.uniq!
|
926
|
+
@pB.say(" Tagged #{@tagged.size} files (#{(@tagsize.to_f / 1_000_000).round(2)}MB)".fg(204))
|
927
|
+
@pB.update = false
|
928
|
+
@pR.update = true
|
929
|
+
end
|
930
|
+
|
931
|
+
def show_tagged # {{{3
|
932
|
+
tagged_info
|
933
|
+
@pB.update = true
|
934
|
+
end
|
935
|
+
|
936
|
+
def clear_tagged # {{{3
|
937
|
+
@tagged = []
|
938
|
+
tagged_info
|
939
|
+
@pB.update = true
|
940
|
+
end
|
941
|
+
|
942
|
+
# MANIPULATE ITEMS {{{2
|
943
|
+
def copy_items # {{{3
|
944
|
+
copy_move_link('copy')
|
945
|
+
@pR.update = true
|
946
|
+
end
|
947
|
+
|
948
|
+
def move_items # {{{3
|
949
|
+
copy_move_link('move')
|
950
|
+
@pR.update = true
|
951
|
+
end
|
952
|
+
|
953
|
+
def rename_item # {{{3
|
954
|
+
basename = File.basename(@selected)
|
955
|
+
dir = File.dirname(@selected)
|
956
|
+
display = basename.length > 12 ? basename[0, 12] + '…' : basename
|
957
|
+
tpl = "mv \"#{display}\" \"#{basename}\""
|
958
|
+
cmd = @pCmd.ask(': ', tpl).pure
|
959
|
+
match = cmd.match(/mv\s+"[^"]+"\s+"([^"]+)"/)
|
960
|
+
new_basename = match ? match[1] : basename
|
961
|
+
old_esc = Shellwords.escape(@selected)
|
962
|
+
new_path = File.join(dir, new_basename)
|
963
|
+
new_esc = Shellwords.escape(new_path)
|
964
|
+
shellexec("mv #{old_esc} #{new_esc}")
|
965
|
+
dirlist
|
966
|
+
# point @selected and @index at the new name
|
967
|
+
@selected = new_path
|
968
|
+
new_idx = @files.index(new_basename)
|
969
|
+
@index = new_idx if new_idx
|
970
|
+
render
|
971
|
+
end
|
972
|
+
|
973
|
+
def link_items # {{{3
|
974
|
+
copy_move_link('link')
|
975
|
+
@pR.update = true
|
976
|
+
end
|
977
|
+
|
978
|
+
def delete_items # {{{3
|
979
|
+
tagged_info
|
980
|
+
@pR.text << "\n\n Selected:\n\n #{@selected}".fg(204).b
|
981
|
+
@pR.refresh
|
982
|
+
# choose wording based on @trash
|
983
|
+
action = @trash ? 'Move (to ~/.rtfm/trash)' : 'Delete'
|
984
|
+
past_action = @trash ? 'Moved' : 'Deleted'
|
985
|
+
@pB.say(" #{action} selected and tagged? (press 'y')")
|
986
|
+
if getchr == 'y'
|
987
|
+
# collect & escape every path
|
988
|
+
paths = (@tagged + [@selected]).uniq
|
989
|
+
esc = paths.map { |p| Shellwords.escape(p) }.join(' ')
|
990
|
+
if @trash
|
991
|
+
esc_trash = Shellwords.escape(TRASH_DIR)
|
992
|
+
command("mv -f #{esc} #{esc_trash}")
|
993
|
+
else
|
994
|
+
command("rm -rf #{esc}")
|
995
|
+
end
|
996
|
+
@tagged.clear
|
997
|
+
refresh_right
|
998
|
+
@pR.say("#{past_action} #{paths.size} items#{@trash ? " to #{TRASH_DIR}" : ''}".fg(204))
|
999
|
+
else
|
1000
|
+
@pB.update = true
|
1001
|
+
end
|
1002
|
+
end
|
1003
|
+
|
1004
|
+
def empty_trash # {{{3
|
1005
|
+
@pB.say(" Really empty Trash (~/.rtfm/trash)? (press 'y')")
|
1006
|
+
return unless getchr == 'y'
|
1007
|
+
|
1008
|
+
command("rm -rf #{TRASH_DIR}/*")
|
1009
|
+
@pB.say('Trash is now empty')
|
1010
|
+
render
|
1011
|
+
end
|
1012
|
+
|
1013
|
+
def toggle_trash # {{{3
|
1014
|
+
@trash = !@trash
|
1015
|
+
@pB.say("Trash (~/.rtfm/trash) = #{@trash ? 'On' : 'Off'}")
|
1016
|
+
end
|
1017
|
+
|
1018
|
+
def change_ownership # {{{3
|
1019
|
+
require 'etc'
|
1020
|
+
gnm = Etc.getgrgid(File.stat(@selected).gid).name
|
1021
|
+
unm = Etc.getpwuid(File.stat(@selected).uid).name
|
1022
|
+
ans = @pB.ask('Change ownership (user:group): ', "#{unm}:#{gnm}")
|
1023
|
+
user, group = ans.split(':')
|
1024
|
+
uid = Etc.getpwnam(user).uid
|
1025
|
+
gid = Etc.getgrnam(group).gid
|
1026
|
+
File.chown(uid, gid, @selected)
|
1027
|
+
@tagged.each { |t| File.chown(uid, gid, t) rescue nil }
|
1028
|
+
if user == unm && group == gnm
|
1029
|
+
@pB.say('No change in ownership')
|
1030
|
+
else
|
1031
|
+
@pB.say("Ownership changed to #{user}:#{group}")
|
1032
|
+
end
|
1033
|
+
end
|
1034
|
+
|
1035
|
+
def change_permissions # {{{3
|
1036
|
+
# strip leading “-” off e.g. "-rwxr-xr-x" → "rwxr-xr-x"
|
1037
|
+
default = @fileattr.split[1][1..]
|
1038
|
+
ans = @pB.ask('Permissions: ', default)
|
1039
|
+
mode = if ans =~ /^\d{3}$/ # "755"
|
1040
|
+
ans.to_i(8)
|
1041
|
+
elsif ans.length == 3 # "rwx" → "rwxrwxrwx"
|
1042
|
+
# compute the single octal digit
|
1043
|
+
digit = ans.chars.map do |c|
|
1044
|
+
(c == 'r' ? 4 : 0) +
|
1045
|
+
(c == 'w' ? 2 : 0) +
|
1046
|
+
(c == 'x' ? 1 : 0)
|
1047
|
+
end.sum
|
1048
|
+
# replicate for user, group, other
|
1049
|
+
(digit * 64) + (digit * 8) + digit
|
1050
|
+
elsif ans.length == 9 # "rwxr-xr-x"
|
1051
|
+
# split into three triads and compute each
|
1052
|
+
triads = ans.scan(/.{3}/)
|
1053
|
+
perms = triads.map do |tri|
|
1054
|
+
(tri[0] == 'r' ? 4 : 0) +
|
1055
|
+
(tri[1] == 'w' ? 2 : 0) +
|
1056
|
+
(tri[2] == 'x' ? 1 : 0)
|
1057
|
+
end
|
1058
|
+
(perms[0] * 64) + (perms[1] * 8) + perms[2]
|
1059
|
+
end
|
1060
|
+
if mode.nil?
|
1061
|
+
@pB.say('Invalid mode')
|
1062
|
+
else
|
1063
|
+
current = File.stat(@selected).mode & 0o777
|
1064
|
+
if mode == current
|
1065
|
+
@pB.say("No change needed (already #{mode.to_s(8)})")
|
1066
|
+
else
|
1067
|
+
File.chmod(mode, @selected)
|
1068
|
+
@tagged.each { |t| File.chmod(mode, t) rescue nil }
|
1069
|
+
@pB.say("Permissions changed to: #{mode.to_s(8)}")
|
1070
|
+
end
|
1071
|
+
end
|
1072
|
+
end
|
1073
|
+
|
1074
|
+
# FILTER AND SEARCH {{{2
|
1075
|
+
def filter_types # {{{3
|
1076
|
+
@lsfiles = @pB.ask('Filetype(s) to show: ', @lsfiles)
|
1077
|
+
@pR.update = @pB.update = true
|
1078
|
+
end
|
1079
|
+
|
1080
|
+
def filter_regex # {{{3
|
1081
|
+
@lsmatch = @pB.ask('Files match RegEx: ', @lsmatch)
|
1082
|
+
@pB.say(nil)
|
1083
|
+
@pR.update = @pB.update = true
|
1084
|
+
end
|
1085
|
+
|
1086
|
+
def filter_clear # {{{3
|
1087
|
+
@lsfiles = @lsmatch = ''
|
1088
|
+
@pB.say('All filtering cleared.')
|
1089
|
+
end
|
1090
|
+
|
1091
|
+
def search_text # {{{3
|
1092
|
+
@searched = @pSearch.ask('/ ', '')
|
1093
|
+
l = command("ls #{@lsbase} #{@lsall} #{@lsorder} #{@lsinvert} #{@lsuser}").split
|
1094
|
+
m = l.each_index.select { |n| l[n] =~ /#{@searched}/ }
|
1095
|
+
@index = m.first unless m.empty?
|
1096
|
+
@index = 0 if @searched.empty?
|
1097
|
+
@pR.update = true; @pB.full_refresh
|
1098
|
+
end
|
1099
|
+
|
1100
|
+
def clear_search # {{{3
|
1101
|
+
@searched = ''
|
1102
|
+
end
|
1103
|
+
|
1104
|
+
def search_next # {{{3
|
1105
|
+
l = command("ls #{@lsbase} #{@lsall} #{@lsorder} #{@lsinvert} #{@lsuser}").split
|
1106
|
+
m = l.each_index.select { |n| l[n] =~ /#{@searched}/ }
|
1107
|
+
i = m.find { |n| n > @index }
|
1108
|
+
@index = i || m.first
|
1109
|
+
@pR.update = true
|
1110
|
+
end
|
1111
|
+
|
1112
|
+
def search_prev # {{{3
|
1113
|
+
l = command("ls #{@lsbase} #{@lsall} #{@lsorder} #{@lsinvert} #{@lsuser}").split
|
1114
|
+
m = l.each_index.select { |n| l[n] =~ /#{@searched}/ }.reverse
|
1115
|
+
i = m.find { |n| n < @index }
|
1116
|
+
@index = i || m.first
|
1117
|
+
@pR.update = true
|
1118
|
+
end
|
1119
|
+
|
1120
|
+
def grep_current # {{{3
|
1121
|
+
cmd = @pCmd.ask(': ', 'grep -s MATCH *')
|
1122
|
+
shellexec(cmd)
|
1123
|
+
end
|
1124
|
+
|
1125
|
+
def locate # {{{3
|
1126
|
+
cmd = @pCmd.ask(': ', 'locate ')
|
1127
|
+
cmd += " | #{@bat} -n --color=always"
|
1128
|
+
shellexec(cmd)
|
1129
|
+
@locate = true
|
1130
|
+
end
|
1131
|
+
|
1132
|
+
def jump_locate # {{{3
|
1133
|
+
@pB.update = true; return unless @locate
|
1134
|
+
|
1135
|
+
nr = @pB.ask('# ', '').to_i
|
1136
|
+
line = @pR.text.lines[nr - 1]
|
1137
|
+
unless line
|
1138
|
+
@pB.say('Error: No such file or directory.'); @pB.update = false; return
|
1139
|
+
end
|
1140
|
+
jump = line.pure[%r{/\S+}]
|
1141
|
+
dir = File.dirname(jump); tgt = File.basename(jump)
|
1142
|
+
@directory[Dir.pwd] = @index; mark_latest; Dir.chdir(dir); @dir_old = Dir.pwd
|
1143
|
+
l = command("ls #{@lsbase} #{@lsall} #{@lsorder} #{@lsinvert} #{@lsuser}").split("\n")
|
1144
|
+
@index = l.index { |t| t == tgt }
|
1145
|
+
end
|
1146
|
+
|
1147
|
+
def fzf_jump # {{{ 3
|
1148
|
+
unless system('which fzf > /dev/null 2>&1')
|
1149
|
+
@pB.say(' fzf not installed – see https://github.com/junegunn/fzf')
|
1150
|
+
return
|
1151
|
+
end
|
1152
|
+
# 2) Prepare a temp file for the single-line selection
|
1153
|
+
tmp = File.join(Dir.tmpdir, 'rtfm_fzf_selection')
|
1154
|
+
# Launch fzf:
|
1155
|
+
# - stdin <- /dev/tty so fzf reads from terminal
|
1156
|
+
# - stdout -> tmp to capture only the chosen entry
|
1157
|
+
# - stderr -> /dev/tty so fzf can draw its full-screen UI
|
1158
|
+
system("fzf < /dev/tty > #{Shellwords.escape(tmp)} 2> /dev/tty")
|
1159
|
+
jump = File.exist?(tmp) ? File.read(tmp).chomp : ''
|
1160
|
+
File.delete(tmp)
|
1161
|
+
return if jump.empty?
|
1162
|
+
|
1163
|
+
dir = File.dirname(jump)
|
1164
|
+
tgt = File.basename(jump)
|
1165
|
+
@directory[Dir.pwd] = @index
|
1166
|
+
mark_latest
|
1167
|
+
Dir.chdir(dir)
|
1168
|
+
@dir_old = Dir.pwd
|
1169
|
+
l = command("ls #{@lsbase} #{@lsall} #{@lsorder} #{@lsinvert} #{@lsuser}").split("\n")
|
1170
|
+
@index = l.index { |t| t == tgt } || 0
|
1171
|
+
@pL.update = @pR.update = @pB.update = true
|
1172
|
+
end
|
1173
|
+
|
1174
|
+
# ARCHIVES {{{2
|
1175
|
+
def unzip_items # {{{3
|
1176
|
+
first = @tagged.first
|
1177
|
+
tar = Shellwords.escape(first)
|
1178
|
+
cmd = @pCmd.ask('Command: ', "tar xfz #{tar}")
|
1179
|
+
shellexec(cmd)
|
1180
|
+
render
|
1181
|
+
end
|
1182
|
+
|
1183
|
+
def zip_items # {{{3
|
1184
|
+
arc = @pCmd.ask('Archive name: ', '')
|
1185
|
+
# escape archive base and all paths
|
1186
|
+
arc_esc = Shellwords.escape(arc)
|
1187
|
+
tagged_esc = @tagged.map { |p| Shellwords.escape(p) }.join(' ')
|
1188
|
+
cmd = @pCmd.ask('Command: ', "tar cfz #{arc_esc}.gz #{tagged_esc}")
|
1189
|
+
shellexec(cmd + ' 2>/dev/null')
|
1190
|
+
render
|
1191
|
+
end
|
1192
|
+
|
1193
|
+
# GIT/HASH/OPENAI {{{2
|
1194
|
+
def git_status # {{{3
|
1195
|
+
@pR.clear
|
1196
|
+
out, err = command('git status', return_both: true)
|
1197
|
+
@pR.text += err.fg(196) unless err.empty?
|
1198
|
+
@pR.text += out.fg(214) unless out.empty?
|
1199
|
+
@pR.full_refresh
|
1200
|
+
@pR.update = false
|
1201
|
+
@pB.update = true
|
1202
|
+
end
|
1203
|
+
|
1204
|
+
def hash_directory # {{{3
|
1205
|
+
dir = Shellwords.escape(Dir.pwd)
|
1206
|
+
@pB.say("Creating hash for #{dir}. May take a while…")
|
1207
|
+
hashcmd = <<~CMD.chomp
|
1208
|
+
(find #{dir} -type f -print0 |
|
1209
|
+
sort -z |
|
1210
|
+
xargs -0 sha1sum;
|
1211
|
+
find #{dir} \\( -type f -o -type d \\) -print0 |
|
1212
|
+
sort -z |
|
1213
|
+
xargs -0 stat -c '%n %a') |
|
1214
|
+
sha1sum |
|
1215
|
+
cut -c -40
|
1216
|
+
CMD
|
1217
|
+
hashdir = command(hashcmd, timeout: 300).chomp
|
1218
|
+
hashtime = DateTime.now.strftime('%Y-%m-%d %H:%M')
|
1219
|
+
if @hash.key?(Dir.pwd)
|
1220
|
+
old_time, old_hash = @hash[Dir.pwd]
|
1221
|
+
if old_hash == hashdir
|
1222
|
+
@pB.say(" Hash for #{Dir.pwd} has NOT changed since #{old_time} (#{hashdir})".fg(213))
|
1223
|
+
else
|
1224
|
+
@pB.say(" Hash for #{Dir.pwd} has CHANGED: #{old_time}→#{hashtime} (#{old_hash}→#{hashdir})".fg(213))
|
1225
|
+
@hash[Dir.pwd] = [hashtime, hashdir]
|
1226
|
+
end
|
1227
|
+
else
|
1228
|
+
@hash[Dir.pwd] = [hashtime, hashdir]
|
1229
|
+
@pB.say(" New hash for #{Dir.pwd}: #{hashtime}: #{hashdir}".fg(213))
|
1230
|
+
end
|
1231
|
+
@pR.update = true
|
1232
|
+
end
|
1233
|
+
|
1234
|
+
def openai_description # {{{3
|
1235
|
+
begin
|
1236
|
+
require 'ruby/openai'
|
1237
|
+
rescue LoadError
|
1238
|
+
@pB.say('To enable AI-descriptions: `gem install ruby-openai` and set @ai in ~/.rtfm/conf')
|
1239
|
+
return
|
1240
|
+
end
|
1241
|
+
unless @ai && !@ai.empty?
|
1242
|
+
@pB.say("Set your API key in ~/.rtfm/conf: @ai = 'your-secret-openai-key'")
|
1243
|
+
return
|
1244
|
+
end
|
1245
|
+
# Context
|
1246
|
+
path = File.join(Dir.pwd, @selected.to_s)
|
1247
|
+
is_dir = File.directory?(path)
|
1248
|
+
preview = @pR.text.pure.strip
|
1249
|
+
# Enhanced prompt
|
1250
|
+
parts = []
|
1251
|
+
parts << 'You are an expert terminal file manager assistant.'
|
1252
|
+
parts << (is_dir ? "Summarize the overall purpose and contents of this directory: #{path}." : "Summarize the purpose of this file: #{path}.")
|
1253
|
+
parts << 'Since this is source code, do a brief code review: highlight bugs, style issues, and improvements.' if !is_dir && path.match?(/\.(sh|rb|py|js|go|java|41)$/i)
|
1254
|
+
parts << 'Embedded Documentation Lookup: for any libraries, commands, or APIs used, include a one-sentence summary from their official docs.'
|
1255
|
+
parts << "Git-Aware Diff Explanation: summarize the most recent `git diff` touching #{path}, explaining what changed." if Dir.exist?(File.join(Dir.pwd, '.git'))
|
1256
|
+
parts << "Existing preview text: #{preview}" unless preview.empty?
|
1257
|
+
prompt = parts.join(' ')
|
1258
|
+
# Send to OpenAI
|
1259
|
+
client = OpenAI::Client.new(access_token: @ai)
|
1260
|
+
@pR.say('Thinking...'.fg(244))
|
1261
|
+
response = client.chat(
|
1262
|
+
parameters: {
|
1263
|
+
model: @aimodel,
|
1264
|
+
messages: [{ role: 'user', content: prompt }],
|
1265
|
+
max_tokens: 600
|
1266
|
+
}
|
1267
|
+
) rescue nil
|
1268
|
+
answer = response&.dig('choices', 0, 'message', 'content') ||
|
1269
|
+
'⚠️ Error or empty response from OpenAI.'
|
1270
|
+
@pR.say(answer.fg(230))
|
1271
|
+
end
|
1272
|
+
|
1273
|
+
def chat_mode # {{{3
|
1274
|
+
unless defined?(OpenAI) && @ai && !@ai.empty?
|
1275
|
+
@pB.say("To make OpenAI work in RTFM, run `gem install ruby-openai` and add to ~/.rtfm/conf:\n @ai = 'your-secret-openai-key'")
|
1276
|
+
return
|
1277
|
+
end
|
1278
|
+
|
1279
|
+
@pB.clear; @pB.update = true
|
1280
|
+
question = @pAI.ask('Chat> ', '').strip
|
1281
|
+
return if question.empty?
|
1282
|
+
|
1283
|
+
chat_history << { role: 'user', content: question }
|
1284
|
+
@pR.say('Thinking...'.fg(230))
|
1285
|
+
reply = openai_client.chat(
|
1286
|
+
parameters: {
|
1287
|
+
model: @aimodel,
|
1288
|
+
messages: chat_history,
|
1289
|
+
max_tokens: 400
|
1290
|
+
}
|
1291
|
+
) rescue nil
|
1292
|
+
answer = reply&.dig('choices', 0, 'message', 'content') ||
|
1293
|
+
'⚠️ API error or empty response'
|
1294
|
+
chat_history << { role: 'assistant', content: answer }
|
1295
|
+
@pR.say(answer.fg(230))
|
1296
|
+
@pB.clear; @pB.update = true
|
1297
|
+
end
|
1298
|
+
|
1299
|
+
# RIGHT PANE CONTROLS {{{2
|
1300
|
+
def refresh_right # {{{3
|
1301
|
+
render
|
1302
|
+
@pR.full_refresh
|
1303
|
+
@pR.update = @pB.update = true
|
1304
|
+
end
|
1305
|
+
|
1306
|
+
def line_down_right # {{{3
|
1307
|
+
@pR.linedown; @pB.update = true
|
1308
|
+
end
|
1309
|
+
|
1310
|
+
def line_up_right # {{{3
|
1311
|
+
@pR.lineup; @pB.update = true
|
1312
|
+
end
|
1313
|
+
|
1314
|
+
def page_down_right # {{{3
|
1315
|
+
@pR.pagedown; @pB.update = true
|
1316
|
+
end
|
1317
|
+
|
1318
|
+
def page_up_right # {{{3
|
1319
|
+
@pR.pageup; @pB.update = true
|
1320
|
+
end
|
1321
|
+
|
1322
|
+
# CLIPBOARD COPY {{{2
|
1323
|
+
def copy_path # {{{3
|
1324
|
+
if @selected
|
1325
|
+
clip = "xclip -selection #{_chr == 'Y' ? 'clipboard' : 'primary'} -in -loops 1"
|
1326
|
+
@pB.say(' Path copied')
|
1327
|
+
shell("echo -n '#{@selected}' | #{clip}")
|
1328
|
+
else
|
1329
|
+
@pB.say(' No selected item path to copy')
|
1330
|
+
end
|
1331
|
+
end
|
1332
|
+
|
1333
|
+
def copy_right # {{{3
|
1334
|
+
clip = 'xclip -selection clipboard'
|
1335
|
+
@pB.say(' Right pane copied to clipboard')
|
1336
|
+
# We drop the trailing & in the string itself…
|
1337
|
+
shell("echo -n #{Shellwords.escape(@pR.text.pure)} | #{clip} > /dev/null 2>&1", background: true)
|
1338
|
+
end
|
1339
|
+
|
1340
|
+
# SYSTEM SHORTCUTS {{{2
|
1341
|
+
def system_info # {{{3
|
1342
|
+
text = ''
|
1343
|
+
begin
|
1344
|
+
uname = `uname -o`.chomp + ' '
|
1345
|
+
uname += `uname -r`.chomp + ' '
|
1346
|
+
uname += `uname -v`.chomp + ' '
|
1347
|
+
uname += `uname -p`.chomp + ' '
|
1348
|
+
uname += `awk -F '"' '/PRETTY/ {print $2}' /etc/os-release` + "\n"
|
1349
|
+
text += uname.b.fg(253)
|
1350
|
+
host = `hostnamectl`
|
1351
|
+
chost = host.lines.map do |line|
|
1352
|
+
if line.include?(':')
|
1353
|
+
before, after = line.split(':', 2)
|
1354
|
+
"#{(before + ':').fg(253)}#{after.fg(111)}"
|
1355
|
+
else
|
1356
|
+
line
|
1357
|
+
end
|
1358
|
+
end.join
|
1359
|
+
text += chost + "\n"
|
1360
|
+
rescue # rubocop:disable Lint/SuppressedException
|
1361
|
+
end
|
1362
|
+
begin
|
1363
|
+
system = 'Shell & Terminal: ' + `echo $SHELL`.sub(%r{.*/}, '').chomp + ', ' + `echo $TERM`.chomp + ' '
|
1364
|
+
packages = `pacman -Q 2>/dev/null | wc -l`.chomp
|
1365
|
+
packages = `dpkg-query -l 2>/dev/null | grep -c '^.i'`.chomp if packages == '0'
|
1366
|
+
packages = 'Unrecognized' if packages == '0'
|
1367
|
+
cpu = 'CPUs = ' + `nproc`.chop + ' '
|
1368
|
+
cpuinfo = `lscpu`
|
1369
|
+
cpu += cpuinfo[/^.*Model name:\s*(.*)/, 1] + ' '
|
1370
|
+
cpu += 'Max: ' + cpuinfo[/^.*CPU max MHz:\s*(.*)/, 1].to_i.to_s + 'MHz '
|
1371
|
+
cpu += 'Min: ' + cpuinfo[/^.*CPU min MHz:\s*(.*)/, 1].to_i.to_s + "MHz\n\n"
|
1372
|
+
text += cpu.fg(154)
|
1373
|
+
system += 'Packages: ' + packages + "\n"
|
1374
|
+
system += 'Desktop: ' + `awk '/^DesktopNames/' /usr/share/xsessions/* | sed 's/DesktopNames=//g' | \\
|
1375
|
+
sed 's/\\;/\\n/g' | sed '/^$/d' | sort -u | sed ':a;N;$!ba;s/\\n/, /g'`.chomp + '/'
|
1376
|
+
system += `grep 'gtk-theme-name' ~/.config/gtk-3.0/* | sed 's/gtk-theme-name=//g' | \\
|
1377
|
+
sed 's/-/ /g'`.sub(/.*:/, '') + "\n"
|
1378
|
+
text += system.fg(251)
|
1379
|
+
rescue # rubocop:disable Lint/SuppressedException
|
1380
|
+
end
|
1381
|
+
begin
|
1382
|
+
mem = `free -h` + "\n"
|
1383
|
+
text += mem.fg(229)
|
1384
|
+
rescue # rubocop:disable Lint/SuppressedException
|
1385
|
+
end
|
1386
|
+
begin
|
1387
|
+
ps = `ps -eo comm,pid,user,pcpu,pmem,stat --sort -pcpu,-pmem | head` + "\n"
|
1388
|
+
text += ps.fg(195)
|
1389
|
+
rescue # rubocop:disable Lint/SuppressedException
|
1390
|
+
end
|
1391
|
+
begin
|
1392
|
+
disk = `df -H | head -8`
|
1393
|
+
text += disk.fg(172)
|
1394
|
+
rescue # rubocop:disable Lint/SuppressedException
|
1395
|
+
end
|
1396
|
+
begin
|
1397
|
+
dmesg = "\nDMESG (latest first):\n"
|
1398
|
+
dcmd = `dmesg 2>/dev/null | tail -6`.split("\n").sort.reverse.join("\n")
|
1399
|
+
dmesg += dcmd == '' ? "dmesg requires root, run 'sudo sysctl kernel.dmesg_restrict=0' if you need permission\n" : dcmd
|
1400
|
+
text += dmesg.fg(219)
|
1401
|
+
rescue # rubocop:disable Lint/SuppressedException
|
1402
|
+
end
|
1403
|
+
@pR.say(text)
|
1404
|
+
rescue
|
1405
|
+
@pR.say('Unable to show system info')
|
1406
|
+
end
|
1407
|
+
|
1408
|
+
def make_directory # {{{3
|
1409
|
+
cmd = @pCmd.ask(': ', 'mkdir ')
|
1410
|
+
shellexec(cmd + ' -p')
|
1411
|
+
end
|
1412
|
+
|
1413
|
+
def navi_invoke # {{{3
|
1414
|
+
@navi = `navi`
|
1415
|
+
rescue
|
1416
|
+
@pB.say(' navi not installed - see https://github.com/junegunn/fzf')
|
1417
|
+
end
|
1418
|
+
|
1419
|
+
# COMMAND MODE {{{2
|
1420
|
+
def command_mode # {{{3
|
1421
|
+
raw = @pCmd.ask('Shell command: ', '').strip
|
1422
|
+
@pB.clear; @pB.update = true
|
1423
|
+
return if raw.empty?
|
1424
|
+
|
1425
|
+
# Prefix override: §cmd = force interactive
|
1426
|
+
force = raw.start_with?('§')
|
1427
|
+
raw = raw[1..].strip if force
|
1428
|
+
# Expand @s / @t
|
1429
|
+
sel = Shellwords.escape(@selected.to_s)
|
1430
|
+
tg = @tagged.map { |p| Shellwords.escape(p) }.join(' ')
|
1431
|
+
cmd = raw.gsub('@s', sel).gsub('@t', tg)
|
1432
|
+
# Determine program name
|
1433
|
+
prog = Shellwords.split(cmd).first
|
1434
|
+
prog = File.basename(prog) if prog
|
1435
|
+
# Whitelist check against @interactive
|
1436
|
+
inter_list = (@interactive || '').split(',').map(&:strip)
|
1437
|
+
whitelist = prog && inter_list.include?(prog)
|
1438
|
+
# Magic PTY-peek (only if not forced or whitelisted)
|
1439
|
+
magic = false
|
1440
|
+
unless force || whitelist
|
1441
|
+
begin
|
1442
|
+
PTY.spawn(cmd) do |r, _w, pid|
|
1443
|
+
begin
|
1444
|
+
Timeout.timeout(0.1) do
|
1445
|
+
magic = r.readpartial(1024).include?("\e[?1049h")
|
1446
|
+
end
|
1447
|
+
rescue Timeout::Error
|
1448
|
+
# no quick data = assume non-TUI
|
1449
|
+
ensure
|
1450
|
+
Process.kill('TERM', pid) rescue nil
|
1451
|
+
Process.wait(pid) rescue nil
|
1452
|
+
end
|
1453
|
+
end
|
1454
|
+
rescue Errno::ENOENT, Errno::EIO
|
1455
|
+
# command not found or PTY closed immediately = non-TUI
|
173
1456
|
end
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
#
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
return "$code"
|
192
|
-
fi
|
193
|
-
d=$(<"$f")
|
194
|
-
rm -f "$f"
|
195
|
-
cd "$d"
|
196
|
-
}
|
197
|
-
RTFMLAUNCH
|
198
|
-
File.write(Dir.home+'/.rtfm.launch', @rtfmlaunch)
|
199
|
-
`echo "source ~/.rtfm.launch" >> .#{@shell}rc`
|
1457
|
+
end
|
1458
|
+
if force || whitelist || magic # Decide interactive vs non-interactive
|
1459
|
+
# Restore shell tty so Ctrl-C/D work
|
1460
|
+
system("stty #{ORIG_STTY} < /dev/tty")
|
1461
|
+
# Clear to top-left
|
1462
|
+
system('clear < /dev/tty > /dev/tty')
|
1463
|
+
Cursor.show
|
1464
|
+
# Spawn on real tty
|
1465
|
+
pid2 = Process.spawn(cmd,
|
1466
|
+
in: '/dev/tty',
|
1467
|
+
out: '/dev/tty',
|
1468
|
+
err: '/dev/tty')
|
1469
|
+
begin
|
1470
|
+
Process.wait(pid2)
|
1471
|
+
rescue Interrupt
|
1472
|
+
Process.kill('TERM', pid2) rescue nil
|
1473
|
+
retry
|
200
1474
|
end
|
1475
|
+
# Restore raw/no-echo for RTFM
|
1476
|
+
system('stty raw -echo isig < /dev/tty')
|
1477
|
+
$stdin.raw!
|
1478
|
+
$stdin.echo = false
|
1479
|
+
Cursor.hide
|
1480
|
+
Rcurses.clear_screen
|
1481
|
+
refresh
|
1482
|
+
render
|
1483
|
+
else
|
1484
|
+
shellexec(cmd)
|
1485
|
+
@pR.refresh
|
1486
|
+
@pR.update = false
|
201
1487
|
end
|
1488
|
+
end
|
202
1489
|
|
203
|
-
|
204
|
-
|
205
|
-
|
1490
|
+
def show_history # {{{3
|
1491
|
+
@pR.say("Command history:\n\n" + @pCmd.history.reverse.join("\n"))
|
1492
|
+
@pB.update = true
|
206
1493
|
end
|
207
|
-
begin # BASIC SETUP
|
208
|
-
require 'fileutils'
|
209
|
-
require 'io/console'
|
210
|
-
require 'io/wait'
|
211
|
-
require 'date'
|
212
|
-
require 'timeout'
|
213
|
-
require 'curses'
|
214
|
-
include Curses
|
215
1494
|
|
216
|
-
|
1495
|
+
def add_interactive # {{{
|
1496
|
+
@interactive = @pB.ask('Add program to @interactive: '.fg(213), @interactive.fg(213))
|
1497
|
+
@pB.clear; @pB.update = true
|
1498
|
+
end
|
217
1499
|
|
218
|
-
|
219
|
-
|
1500
|
+
# RUBY MODE {{{2
|
1501
|
+
def ruby_debug # {{{3
|
1502
|
+
require 'stringio'
|
1503
|
+
cmd = @pRuby.ask('Ruby command: ', '')
|
1504
|
+
@pR.text = "Command: #{cmd}\n\n".fg(205)
|
1505
|
+
original_stdout = $stdout
|
1506
|
+
original_stderr = $stderr
|
1507
|
+
stdout_captured = StringIO.new
|
1508
|
+
stderr_captured = StringIO.new
|
1509
|
+
$stdout = stdout_captured
|
1510
|
+
$stderr = stderr_captured
|
1511
|
+
begin
|
1512
|
+
eval(cmd) # rubocop:disable Security/Eval
|
1513
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
1514
|
+
puts "Error: #{e.message}\n#{e.backtrace.join("\n")}"
|
1515
|
+
ensure
|
1516
|
+
$stdout = original_stdout
|
1517
|
+
$stderr = original_stderr
|
220
1518
|
end
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
1519
|
+
@pR.text += stdout_captured.string + "\n\n" + stderr_captured.string
|
1520
|
+
@pR.refresh
|
1521
|
+
@pR.update = false
|
1522
|
+
@pB.full_refresh
|
1523
|
+
end
|
1524
|
+
|
1525
|
+
# GENERIC FUNCTIONS {{{1
|
1526
|
+
def dirlist(left: true) # LIST DIRECTORIES {{{2
|
1527
|
+
@index ||= 0
|
1528
|
+
@index = @index.to_i
|
1529
|
+
if left
|
1530
|
+
dir = Dir.pwd
|
1531
|
+
width = @pL.w
|
225
1532
|
else
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
@
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
@
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
@
|
258
|
-
@
|
259
|
-
@
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
@docfile = /\.doc$/
|
282
|
-
@docxfile = /\.docx$/
|
283
|
-
@xlsxfile = /\.xlsx$/
|
284
|
-
@pptxfile = /\.pptx$/
|
285
|
-
@oolofile = /\.odt$|\.odc$|\.odp$|\.odg$/
|
286
|
-
@pdffile = /\.pdf$|\.ps$/
|
287
|
-
## Get variables from config file (written back to .rtf.conf upon exit via 'q')
|
288
|
-
if File.exist?(Dir.home+'/.rtfm.conf')
|
289
|
-
load(Dir.home+'/.rtfm.conf')
|
290
|
-
end
|
291
|
-
Dir.chdir(ARGV[0]) if ARGV[0] and Dir.exist?(ARGV[0]) and ARGV[0] !~ /\/tmp/
|
292
|
-
end
|
293
|
-
# CLASS EXTENSIONS
|
294
|
-
class Curses::Window
|
295
|
-
attr_accessor :fg, :bg, :attr, :text, :update, :pager, :pager_more, :pager_cmd, :locate, :nohistory
|
296
|
-
# General extensions (see https://github.com/isene/Ruby-Curses-Class-Extension)
|
297
|
-
def clr
|
298
|
-
self.setpos(0, 0)
|
299
|
-
self.maxy.times {self.deleteln()}
|
300
|
-
self.refresh
|
301
|
-
self.setpos(0, 0)
|
302
|
-
end
|
303
|
-
def fill # Fill window with color as set by :bg
|
304
|
-
self.setpos(0, 0)
|
305
|
-
self.bg = 0 if self.bg == nil
|
306
|
-
self.fg = 255 if self.fg == nil
|
307
|
-
init_pair(self.fg, self.fg, self.bg)
|
308
|
-
blank = " " * self.maxx
|
309
|
-
self.maxy.times {self.attron(color_pair(self.fg)) {self << blank}}
|
310
|
-
self.refresh
|
311
|
-
self.setpos(0, 0)
|
312
|
-
end
|
313
|
-
def write # Write context of :text to window with attributes :attr
|
314
|
-
self.bg = 0 if self.bg == nil
|
315
|
-
self.fg = 255 if self.fg == nil
|
316
|
-
init_pair(self.fg, self.fg, self.bg)
|
317
|
-
self.attr = 0 if self.attr == nil
|
318
|
-
self.attron(color_pair(self.fg) | self.attr) { self << self.text }
|
319
|
-
self.refresh
|
320
|
-
end
|
321
|
-
# RTFM specific extensions
|
322
|
-
end
|
323
|
-
# GENERIC FUNCTIONS
|
324
|
-
def syntax_highlight(input) # SYNTAX HIGHLIGHTING FROM ANSI COLOR CODES
|
325
|
-
color_ary = color_parse(input)
|
326
|
-
color_ary.each do | pair |
|
327
|
-
begin
|
328
|
-
fg = pair[0].to_i
|
329
|
-
atr = pair[1].to_i
|
330
|
-
atr == 1 ? atr = Curses::A_BOLD : atr = 0
|
331
|
-
text = pair[2]
|
332
|
-
text.gsub!(/\t/, '')
|
333
|
-
init_pair(fg, fg, 0)
|
334
|
-
@w_r.attron(color_pair(fg) | atr) { @w_r << text }
|
335
|
-
rescue
|
1533
|
+
dir = if @selected && File.directory?(@selected)
|
1534
|
+
File.symlink?(@selected) ? File.realpath(@selected) : @selected.to_s
|
1535
|
+
else
|
1536
|
+
File.dirname(@selected)
|
1537
|
+
end
|
1538
|
+
width = @pR.w
|
1539
|
+
end
|
1540
|
+
# Fetch plain names + colored lines
|
1541
|
+
purels = command("ls #{Shellwords.escape(dir)} #{@lsbase} #{@lsall} #{@lsorder} #{@lsinvert} #{@lsuser}").pure.split("\n")
|
1542
|
+
colorls = command("ls --color #{Shellwords.escape(dir)} #{@lsbase} #{@lsall} #{@lslong} #{@lsorder} #{@lsinvert} #{@lsuser}").split("\n")
|
1543
|
+
colorls.shift if colorls[0]&.strip == "#{dir}:"
|
1544
|
+
colorls.shift if colorls[0]&.match?(/^total/)
|
1545
|
+
if left && @orderchange # Keep the same @selected even when we re-sort
|
1546
|
+
basename = File.basename(@selected.to_s)
|
1547
|
+
new_idx = purels.index(basename)
|
1548
|
+
@index = new_idx if new_idx
|
1549
|
+
@orderchange = false
|
1550
|
+
end
|
1551
|
+
entries = purels.zip(colorls) # Zip into pairs and filter
|
1552
|
+
unless @lsfiles.empty? # filter by extension(s)
|
1553
|
+
exts = @lsfiles.split(',').map { |e| /\.#{Regexp.escape(e)}$/i }
|
1554
|
+
entries.select! { |name, _| exts.any? { |re| name =~ re } }
|
1555
|
+
end
|
1556
|
+
unless @lsmatch.empty? # filter by regex
|
1557
|
+
re = Regexp.new(@lsmatch)
|
1558
|
+
entries.select! { |name, _| name =~ re }
|
1559
|
+
end
|
1560
|
+
# Unzip, with safe defaults if nothing matched
|
1561
|
+
t = entries.transpose
|
1562
|
+
purels = t[0] || []
|
1563
|
+
ls = t[1] || []
|
1564
|
+
@files = purels if left
|
1565
|
+
# Update @selected & @fileattr for left pane
|
1566
|
+
if left && purels[@index]
|
1567
|
+
@selected = Dir.pwd + '/' + purels[@index]
|
1568
|
+
sfile = @selected.dup
|
1569
|
+
sfile += '/' if File.directory?(@selected)
|
1570
|
+
slsl_cmd = "ls -ldHlh --time-style=long-iso #{Shellwords.escape(sfile)}"
|
1571
|
+
slsl = command(slsl_cmd)
|
1572
|
+
a = slsl.split
|
1573
|
+
@fileattr = a.size >= 7 ? "#{a[2]}:#{a[3]} #{a[0]} #{a[4]} #{a[5]} #{a[6]}" : ''
|
1574
|
+
end
|
1575
|
+
# Map & decorate each colored line
|
1576
|
+
ls.map!.with_index do |el, i|
|
1577
|
+
n = el.clean_ansi
|
1578
|
+
n = n.shorten(width - 5).inject('…', -1) if n.pure.length > width - 6
|
1579
|
+
raw_name = (purels[i] || '').strip
|
1580
|
+
base = left ? Dir.pwd : dir
|
1581
|
+
fullpath = "#{base}/#{raw_name}"
|
1582
|
+
n = n.inject('@', -1) if File.symlink?(fullpath)
|
1583
|
+
n = n.inject('/', -1) if File.directory?(fullpath)
|
1584
|
+
n = n.bg(238) if !raw_name.empty? && raw_name.match(/#{@searched}/) && @searched != ''
|
1585
|
+
n = n.r if @tagged.include?(fullpath)
|
1586
|
+
if left
|
1587
|
+
n = (i == @index ? '→ ' + n.u : ' ' + n)
|
336
1588
|
end
|
1589
|
+
n
|
337
1590
|
end
|
1591
|
+
ls.join("\n")
|
338
1592
|
end
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
when "\e" # ANSI escape sequences
|
359
|
-
return "ESC" if $stdin.ready? == nil
|
360
|
-
case $stdin.getc
|
361
|
-
when '[' # CSI
|
362
|
-
case $stdin.getc
|
363
|
-
when 'A' then chr = "UP"
|
364
|
-
when 'B' then chr = "DOWN"
|
365
|
-
when 'C' then chr = "RIGHT"
|
366
|
-
when 'D' then chr = "LEFT"
|
367
|
-
when 'Z' then chr = "S-TAB"
|
368
|
-
when '2' then chr = "INS" ; chr = "C-INS" if $stdin.getc == "^"
|
369
|
-
when '3' then chr = "DEL" ; chr = "C-DEL" if $stdin.getc == "^"
|
370
|
-
when '5' then chr = "PgUP" ; chr = "C-PgUP" if $stdin.getc == "^"
|
371
|
-
when '6' then chr = "PgDOWN" ; chr = "C-PgDOWN" if $stdin.getc == "^"
|
372
|
-
when '7' then chr = "HOME" ; chr = "C-HOME" if $stdin.getc == "^"
|
373
|
-
when '8' then chr = "END" ; chr = "C-END" if $stdin.getc == "^"
|
374
|
-
else chr = ""
|
375
|
-
end
|
376
|
-
when 'O' # Set Ctrl+ArrowKey equal to ArrowKey; May be used for other purposes in the future
|
377
|
-
case $stdin.getc
|
378
|
-
when 'a' then chr = "C-UP"
|
379
|
-
when 'b' then chr = "C-DOWN"
|
380
|
-
when 'c' then chr = "C-RIGHT"
|
381
|
-
when 'd' then chr = "C-LEFT"
|
382
|
-
else chr = ""
|
383
|
-
end
|
384
|
-
end
|
385
|
-
when "", "" then chr = "BACK"
|
386
|
-
when "" then chr = "C-C"
|
387
|
-
when "" then chr = "C-D"
|
388
|
-
when "" then chr = "C-E"
|
389
|
-
when "" then chr = "C-G"
|
390
|
-
when "" then chr = "C-K"
|
391
|
-
when "" then chr = "C-L"
|
392
|
-
when "" then chr = "C-N"
|
393
|
-
when "" then chr = "C-O"
|
394
|
-
when "" then chr = "C-P"
|
395
|
-
when "" then chr = "C-T"
|
396
|
-
when "" then chr = "C-Y"
|
397
|
-
when "" then chr = "WBACK"
|
398
|
-
when "" then chr = "LDEL"
|
399
|
-
when "\r" then chr = "ENTER"
|
400
|
-
when "\t" then chr = "TAB"
|
401
|
-
when /[[:print:]]/ then chr = c
|
402
|
-
else chr = ""
|
403
|
-
end
|
404
|
-
return chr
|
405
|
-
end
|
406
|
-
def main_getkey # GET KEY FROM USER
|
407
|
-
dir = Dir.pwd
|
408
|
-
chr = getchr
|
409
|
-
case chr
|
410
|
-
# BASIC KEYS
|
411
|
-
when '?' # Show helptext in right window
|
412
|
-
@w_r.fg = 249
|
413
|
-
w_r_info(@help)
|
414
|
-
@w_b.update = true
|
415
|
-
when 'r' # Refresh all windows
|
416
|
-
@break = true
|
417
|
-
when 'R' # Reload .rtfm.conf
|
418
|
-
if File.exist?(Dir.home+'/.rtfm.conf')
|
419
|
-
load(Dir.home+'/.rtfm.conf')
|
420
|
-
end
|
421
|
-
w_b_info(" Config reloaded")
|
422
|
-
when 'W' # Write all parameters to .rtfm.conf
|
423
|
-
@write_conf_all = true
|
424
|
-
conf_write
|
425
|
-
@w_b.update = true
|
426
|
-
when 'q' # Exit
|
427
|
-
@tagged = []
|
428
|
-
@write_conf = true
|
429
|
-
exit 0
|
430
|
-
when 'Q' # Exit without writing to .rtfm.conf
|
431
|
-
@tagged = []
|
432
|
-
system("printf \"\033]0;#{Dir.pwd}\007\"")
|
433
|
-
@write_conf = false
|
434
|
-
exit 0
|
435
|
-
when 'v'
|
436
|
-
w_b_info("RTFM version = #{@version} (latest RubyGems version is #{Gem.latest_version_for("rtfm-filemanager").version} - https://github.com/isene/RTFM)")
|
437
|
-
# MOTION
|
438
|
-
when 'DOWN', 'j', 'C-DOWN'
|
439
|
-
var_resets
|
440
|
-
@index = @index >= @max_index ? @min_index : @index + 1
|
441
|
-
@w_r.update = true
|
442
|
-
@w_b.update = true
|
443
|
-
when 'UP', 'k', 'C-UP'
|
444
|
-
var_resets
|
445
|
-
@index = @index <= @min_index ? @max_index : @index - 1
|
446
|
-
@w_r.update = true
|
447
|
-
@w_b.update = true
|
448
|
-
when 'LEFT', 'h', 'C-LEFT'
|
449
|
-
var_resets
|
450
|
-
cur_dir = Dir.pwd
|
451
|
-
@directory[Dir.pwd] = @selected # Store this directory before leaving
|
452
|
-
mark_latest
|
453
|
-
Dir.chdir("..")
|
454
|
-
@directory[Dir.pwd] = File.basename(cur_dir) unless @directory.key?(Dir.pwd)
|
455
|
-
@w_r.update = true
|
456
|
-
@w_b.update = true
|
457
|
-
when 'RIGHT', 'l', 'C-RIGHT'
|
458
|
-
var_resets
|
459
|
-
@directory[Dir.pwd] = @selected # Store this directory before leaving
|
460
|
-
mark_latest
|
461
|
-
open_selected()
|
462
|
-
@w_r.update = true
|
463
|
-
@w_b.update = true
|
464
|
-
when 'x' # Force open with file opener (used to open HTML files in browser)
|
465
|
-
var_resets
|
466
|
-
@directory[Dir.pwd] = @selected # Store this directory before leaving
|
467
|
-
mark_latest
|
468
|
-
open_selected(true)
|
469
|
-
@w_r.update = true
|
470
|
-
@w_b.update = true
|
471
|
-
when 'PgDOWN'
|
472
|
-
var_resets
|
473
|
-
@index += @w_l.maxy - 2
|
474
|
-
@index = @max_index if @index > @max_index
|
475
|
-
@w_r.update = true
|
476
|
-
@w_b.update = true
|
477
|
-
when 'PgUP'
|
478
|
-
var_resets
|
479
|
-
@index -= @w_l.maxy - 2
|
480
|
-
@index = @min_index if @index < @min_index
|
481
|
-
@w_r.update = true
|
482
|
-
@w_b.update = true
|
483
|
-
when 'END'
|
484
|
-
var_resets
|
485
|
-
@index = @max_index
|
486
|
-
@w_r.update = true
|
487
|
-
@w_b.update = true
|
488
|
-
when 'HOME'
|
489
|
-
var_resets
|
490
|
-
@index = @min_index
|
491
|
-
@w_r.update = true
|
492
|
-
@w_b.update = true
|
493
|
-
# JUMPING AND MARKS
|
494
|
-
when 'm' # Set mark
|
495
|
-
marks_info
|
496
|
-
m = $stdin.getc
|
497
|
-
if m.match(/[\w']/)
|
498
|
-
@marks[m] = Dir.pwd
|
499
|
-
elsif m == "-"
|
500
|
-
r = $stdin.getc
|
501
|
-
@marks.delete(r)
|
502
|
-
end
|
503
|
-
marks_info
|
504
|
-
@w_r.update = false
|
505
|
-
@w_b.update = true
|
506
|
-
when 'M' # Show marks
|
507
|
-
marks_info
|
508
|
-
@w_r.update = false
|
509
|
-
@w_b.update = true
|
510
|
-
when "'" # Jump to mark
|
511
|
-
marks_info
|
512
|
-
m = stdscr.getch.to_s
|
513
|
-
if m.match(/[\w']/) and @marks[m]
|
514
|
-
var_resets
|
515
|
-
@directory[Dir.pwd] = @selected # Store this directory before leaving
|
516
|
-
dir_before = Dir.pwd
|
517
|
-
begin
|
518
|
-
Dir.chdir(@marks[m])
|
519
|
-
rescue
|
520
|
-
w_b_info(" No such directory")
|
521
|
-
end
|
522
|
-
mark_latest
|
523
|
-
@marks["'"] = dir_before
|
524
|
-
end
|
525
|
-
@w_r.update = true
|
526
|
-
@w_b.update = true
|
527
|
-
when '~' # Go to home dir
|
528
|
-
var_resets
|
529
|
-
@directory[Dir.pwd] = @selected # Store this directory before leaving
|
530
|
-
mark_latest
|
531
|
-
Dir.chdir
|
532
|
-
@w_r.update = true
|
533
|
-
@w_b.update = true
|
534
|
-
when '>' # Follow symlink
|
535
|
-
@directory[Dir.pwd] = @selected # Store this directory before leaving
|
536
|
-
mark_latest
|
537
|
-
if File.symlink?(@selected)
|
538
|
-
begin
|
539
|
-
Dir.chdir(File.dirname(File.readlink(@selected)))
|
540
|
-
rescue
|
541
|
-
end
|
542
|
-
end
|
543
|
-
@w_b.update = true
|
544
|
-
# SEARCHING
|
545
|
-
when '/' # Get search string to mark items that match the input
|
546
|
-
@w_b.nohistory = true
|
547
|
-
@searched = w_b_getstr("/ ", "")
|
548
|
-
l = `ls 2>/dev/null #{@lsbase} #{@lsall} #{@lsorder} #{@lsinvert} #{@lsuser}`.split
|
549
|
-
m = l.each_index.select{|n| l[n] =~ /#{@searched}/}
|
550
|
-
@index = m[0] unless m == []
|
551
|
-
@index = 0 if @searched == ""
|
552
|
-
@w_r.update = true
|
553
|
-
when '\\' # Clear search string
|
554
|
-
@searched = ""
|
555
|
-
when 'n' # Jump to next occurence of search (after '/')
|
556
|
-
l = `ls 2>/dev/null #{@lsbase} #{@lsall} #{@lsorder} #{@lsinvert} #{@lsuser}`.split
|
557
|
-
m = l.each_index.select{|n| l[n] =~ /#{@searched}/}
|
558
|
-
i = m.find { |n| n > @index }
|
559
|
-
if i == nil
|
560
|
-
@index = m.first
|
561
|
-
else
|
562
|
-
@index = i
|
563
|
-
end
|
564
|
-
@w_r.update = true
|
565
|
-
when 'N' # Jump to previous occurence of search (after '/')
|
566
|
-
l = `ls 2>/dev/null #{@lsbase} #{@lsall} #{@lsorder} #{@lsinvert} #{@lsuser}`.split
|
567
|
-
m = l.each_index.select{|n| l[n] =~ /#{@searched}/}.reverse
|
568
|
-
i = m.find { |n| n < @index }
|
569
|
-
if i == nil
|
570
|
-
@index = m.first
|
571
|
-
else
|
572
|
-
@index = i
|
573
|
-
end
|
574
|
-
@w_r.update = true
|
575
|
-
when 'g' # Run 'grep' in the current directory
|
576
|
-
cmd = w_b_getstr(": ", "grep -s MATCH *")
|
577
|
-
w_b_exec(cmd)
|
578
|
-
when 'L' # Run 'locate' and let user jump to a result (by '#')
|
579
|
-
cmd = w_b_getstr(": ", "locate ")
|
580
|
-
w_b_exec(cmd)
|
581
|
-
@w_r.locate = true
|
582
|
-
@w_b.update = true
|
583
|
-
when '#' # Jump to the line number in list of matches to 'locate'
|
584
|
-
if @w_r.locate
|
585
|
-
jumpnr = w_b_getstr("# ", "").to_i
|
586
|
-
jumpline = @w_r.text.lines[jumpnr - 1]
|
587
|
-
jumpdir = jumpline[/\/[^\e]*/]
|
588
|
-
unless Dir.exist?(jumpdir)
|
589
|
-
@searched = File.basename(jumpdir)
|
590
|
-
jumpdir = File.dirname(jumpdir)
|
591
|
-
end
|
592
|
-
@directory[Dir.pwd] = @selected # Store this directory before leaving
|
593
|
-
mark_latest
|
594
|
-
Dir.chdir(jumpdir)
|
595
|
-
@w_r.pager = 0
|
596
|
-
end
|
597
|
-
@w_b.update = true
|
598
|
-
when 'C-L' # fzf integration (https://github.com/junegunn/fzf)
|
599
|
-
begin
|
600
|
-
jump = `fzf`.chomp
|
601
|
-
rescue
|
602
|
-
w_b_info(" fzf not installed - see https://github.com/junegunn/fzf")
|
1593
|
+
|
1594
|
+
def render # RENDER ALL PANES {{{2
|
1595
|
+
# LEFT pane {{{3
|
1596
|
+
if @pL.update
|
1597
|
+
lefttext = @pL.text
|
1598
|
+
@pL.text = dirlist
|
1599
|
+
scrolloff = 3
|
1600
|
+
total = @files.size
|
1601
|
+
page = @pL.h
|
1602
|
+
if total <= page
|
1603
|
+
# If everything fits, always start from the very top
|
1604
|
+
@pL.ix = 0
|
1605
|
+
elsif @index - @pL.ix < scrolloff
|
1606
|
+
# If we're too close to the top of the pane, scroll up
|
1607
|
+
@pL.ix = [@index - scrolloff, 0].max
|
1608
|
+
elsif (@pL.ix + page - 1 - @index) < scrolloff
|
1609
|
+
# If we're too close to the bottom of the pane, scroll down
|
1610
|
+
max_off = total - page
|
1611
|
+
@pL.ix = [@index + scrolloff - page + 1, max_off].min
|
603
1612
|
end
|
604
|
-
|
605
|
-
@
|
606
|
-
@
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
if @
|
614
|
-
|
615
|
-
|
1613
|
+
@min_index = 0
|
1614
|
+
@max_index = total - 1
|
1615
|
+
@index = 0 if @index.negative?
|
1616
|
+
@index = @max_index if @index > @max_index
|
1617
|
+
@pL.refresh unless @pL.text == lefttext
|
1618
|
+
end
|
1619
|
+
|
1620
|
+
# RIGHT pane {{{3
|
1621
|
+
if @pR.update && @preview
|
1622
|
+
showimage('clear') if @image; @image = false
|
1623
|
+
righttext = @pR.text
|
1624
|
+
if @selected && File.directory?(@selected)
|
1625
|
+
@pR.text = dirlist(left: false)
|
616
1626
|
else
|
617
|
-
|
618
|
-
@tagsize += File.size(item[1...-1])
|
1627
|
+
showcontent
|
619
1628
|
end
|
620
|
-
@
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
@
|
628
|
-
|
629
|
-
|
630
|
-
tagged_info
|
631
|
-
@w_r.update = false
|
632
|
-
@w_b.update = true
|
633
|
-
when 'u' # Clear tagged list
|
634
|
-
@tagged = []
|
635
|
-
tagged_info
|
636
|
-
@w_r.update = false
|
637
|
-
@w_b.update = true
|
638
|
-
# MANIPULATE ITEMS
|
639
|
-
when 'p' # Copy tagged items here
|
640
|
-
copy_move_link("copy")
|
641
|
-
@w_r.update = true
|
642
|
-
when 'P' # Move tagged items here
|
643
|
-
copy_move_link("move")
|
644
|
-
@w_r.update = true
|
645
|
-
when 's' # Create symlink to tagged items here
|
646
|
-
copy_move_link("link")
|
647
|
-
@w_r.update = true
|
648
|
-
when 'd' # Delete items tagged and @selected
|
649
|
-
tagged_info
|
650
|
-
w_b_info(" Delete selected and tagged? (press 'y' to delete)")
|
651
|
-
begin
|
652
|
-
@tagged.push("\"#{Dir.pwd}/#{@selected}\"")
|
653
|
-
@tagged.uniq!
|
654
|
-
deletes = @tagged.join(" ")
|
655
|
-
if $stdin.getc == 'y'
|
656
|
-
`rm -rf #{deletes} 2>/dev/null`
|
657
|
-
items_number = @tagged.length
|
658
|
-
@tagged = []
|
659
|
-
w_b_info("Deleted #{items_number} items: #{deletes}")
|
660
|
-
else
|
661
|
-
@w_b.update = true
|
662
|
-
end
|
663
|
-
@w_r.update = true
|
664
|
-
rescue StandardError => err
|
665
|
-
w_b_info(err.to_s)
|
1629
|
+
@pR.full_refresh unless @pR.text == righttext || @image
|
1630
|
+
end
|
1631
|
+
|
1632
|
+
# TOP PANE {{{3
|
1633
|
+
if @pT.update
|
1634
|
+
toptext = @pT.text
|
1635
|
+
text = ' ' + ENV.fetch('USER') + '@' + `hostname 2>/dev/null`.chomp + ': '
|
1636
|
+
unless @selected.nil?
|
1637
|
+
text += @selected
|
1638
|
+
text += " → #{File.readlink(@selected)}" if File.symlink?(@selected)
|
666
1639
|
end
|
667
|
-
|
668
|
-
|
669
|
-
|
670
|
-
el1 = @selected_safe
|
671
|
-
el2 = cmd.split(' "').last[0..-2]
|
672
|
-
cmd = "mv #{el1} \"#{el2}\""
|
1640
|
+
# File attributes
|
1641
|
+
text += " (#{@fileattr})" if defined?(@fileattr)
|
1642
|
+
# Image or PDF metadata
|
673
1643
|
begin
|
674
|
-
if
|
675
|
-
|
676
|
-
|
677
|
-
|
1644
|
+
if @selected&.match(@imagefile)
|
1645
|
+
if cmd?('identify')
|
1646
|
+
meta = `identify #{Shellwords.escape(@selected)} \
|
1647
|
+
| awk '{printf " [%s %s %s %s] ", $3,$2,$5,$6}' 2>/dev/null`
|
1648
|
+
text += meta
|
1649
|
+
end
|
1650
|
+
elsif @selected&.match(@pdffile)
|
1651
|
+
info = `pdfinfo #{Shellwords.escape(@selected)} 2>/dev/null`
|
1652
|
+
pages = info[/^Pages:\s+(\d+)/, 1]
|
1653
|
+
size = info[/^Page size:.*\((.*)\)/, 1]
|
1654
|
+
text += " [#{pages} pages]" if pages
|
1655
|
+
text += " [#{size}]" if size
|
678
1656
|
end
|
679
|
-
rescue
|
680
|
-
|
681
|
-
end
|
682
|
-
@w_r.update = false
|
683
|
-
when 'C-O' # Change ownerships
|
684
|
-
require 'etc'
|
685
|
-
gnm = Etc.getgrgid(File.stat(@selected).gid).name
|
686
|
-
unm = Etc.getpwuid(File.stat(@selected).uid).name
|
687
|
-
p = "Change ownership of selected"
|
688
|
-
p += " and tagged" unless @tagged.empty?
|
689
|
-
p += ". Selected ownership (user:group) = "
|
690
|
-
own = w_b_getstr(p, "#{unm}:#{gnm}").split(":")
|
691
|
-
gnm, unm = own[0], own[1]
|
692
|
-
begin
|
693
|
-
gid = Etc.getgrnam(gnm).gid.to_i
|
694
|
-
uid = Etc.getpwnam(unm).uid.to_i
|
695
|
-
File.chown(uid, gid, @selected)
|
696
|
-
@tagged.each {|t| File.chown(uid, gid, t[1...-1])} unless @tagged.empty?
|
697
|
-
w_b_info("Ownership changed to #{gnm}:#{unm}")
|
698
|
-
rescue StandardError => err
|
699
|
-
w_b_info(err.to_s)
|
700
|
-
end
|
701
|
-
when 'C-P' # Change permissions
|
702
|
-
mode = @fspes[@index][1..9]
|
703
|
-
p = "Change permissions of selected"
|
704
|
-
p += " and tagged" unless @tagged.empty?
|
705
|
-
p += ". Selected permissions = "
|
706
|
-
mode = w_b_getstr(p, mode)
|
707
|
-
mode = mode * 3 if mode.length == 3 and mode.to_i == 0
|
708
|
-
if mode.length == 9 and mode.to_i == 0
|
709
|
-
x = 0
|
710
|
-
x += 400 if mode[0] == "r"
|
711
|
-
x += 200 if mode[1] == "w"
|
712
|
-
x += 100 if mode[2] == "x"
|
713
|
-
x += 40 if mode[3] == "r"
|
714
|
-
x += 20 if mode[4] == "w"
|
715
|
-
x += 10 if mode[5] == "x"
|
716
|
-
x += 4 if mode[6] == "r"
|
717
|
-
x += 2 if mode[7] == "w"
|
718
|
-
x += 1 if mode[8] == "x"
|
719
|
-
mode = x
|
1657
|
+
rescue Errno::ENOENT, Errno::EACCES
|
1658
|
+
# ignore missing or permission errors
|
720
1659
|
end
|
721
|
-
|
722
|
-
|
1660
|
+
# Directory children count
|
1661
|
+
if @selected && Dir.exist?(@selected)
|
723
1662
|
begin
|
724
|
-
|
725
|
-
|
726
|
-
rescue
|
727
|
-
|
728
|
-
end
|
729
|
-
else
|
730
|
-
w_b_info("Not a valid permissions mode. Nothing changed.")
|
731
|
-
end
|
732
|
-
when 'z' # Unzip selected archive file
|
733
|
-
cmd = w_b_getstr("Command = ", "tar xfz #{@tagged.first}")
|
734
|
-
begin
|
735
|
-
w_b_exec(cmd + " 2>/dev/null")
|
736
|
-
rescue StandardError => err
|
737
|
-
w_b_info(err.to_s)
|
738
|
-
end
|
739
|
-
when 'Z' # Create archive file
|
740
|
-
arc = w_b_getstr("Archive name: ", "")
|
741
|
-
cmd = w_b_getstr("Command = ", "tar cfz #{arc}.gz #{@tagged.join(" ")}")
|
742
|
-
begin
|
743
|
-
w_b_exec(cmd + " 2>/dev/null")
|
744
|
-
rescue StandardError => err
|
745
|
-
w_b_info(err.to_s)
|
746
|
-
end
|
747
|
-
# DIRECTORY VIEWS
|
748
|
-
when 'a' # Show all items
|
749
|
-
@lsall == "" ? @lsall = "-a" : @lsall = ""
|
750
|
-
@w_r.update = true
|
751
|
-
@w_b.update = true
|
752
|
-
when 'A' # Show all info for all items
|
753
|
-
@lslong = !@lslong
|
754
|
-
@w_r.update = true
|
755
|
-
@w_b.update = true
|
756
|
-
when 'o' # Circular toggle the order/sorting of directory views
|
757
|
-
case @lsorder
|
758
|
-
when ""
|
759
|
-
@lsorder = "-S"
|
760
|
-
w_b_info(" Sorting by size, largest first")
|
761
|
-
when "-S"
|
762
|
-
@lsorder = "-t"
|
763
|
-
w_b_info(" Sorting by modification time")
|
764
|
-
when "-t"
|
765
|
-
@lsorder = "-X"
|
766
|
-
w_b_info(" Sorting by extension (alphabetically)")
|
767
|
-
when "-X"
|
768
|
-
@lsorder = ""
|
769
|
-
w_b_info(" Normal sorting")
|
770
|
-
end
|
771
|
-
@w_r.update = true
|
772
|
-
@orderchange = true
|
773
|
-
when 'i' # Invert the order/sorting of directory views
|
774
|
-
case @lsinvert
|
775
|
-
when ""
|
776
|
-
@lsinvert = "-r"
|
777
|
-
w_b_info(" Sorting inverted")
|
778
|
-
when "-r"
|
779
|
-
@lsinvert = ""
|
780
|
-
w_b_info(" Sorting NOT inverted")
|
781
|
-
end
|
782
|
-
@w_r.update = true
|
783
|
-
@orderchange = true
|
784
|
-
when 'O' # Show the Ordering in the bottom window (the full ls command)
|
785
|
-
w_b_info(" Full 'ls' command: ls <@s> #{@lsbase} #{@lsall} #{@lsorder} #{@lsinvert} #{@lsuser}")
|
786
|
-
when 'G' # Git status for selected item or current dir
|
787
|
-
@w_r.fg = 214
|
788
|
-
if File.exist?(".git")
|
789
|
-
w_r_info(`git status 2>/dev/null`)
|
790
|
-
else
|
791
|
-
w_r_info("This is not a git repository.")
|
792
|
-
end
|
793
|
-
@w_r.update = false
|
794
|
-
@w_b.update = true
|
795
|
-
when 'H' # Compare with previous hash status or write hash status if no existing hash
|
796
|
-
@w_r.fg = 213
|
797
|
-
hashcmd = "\(find #{Dir.pwd} -type f -print0 | sort -z | xargs -0 sha1sum; find #{Dir.pwd}"\
|
798
|
-
" \\( -type f -o -type d \\) -print0 | sort -z | xargs -0 stat -c '%n %a'\) | sha1sum | cut -c -40"
|
799
|
-
begin
|
800
|
-
hashdir = `#{hashcmd}`.chomp
|
801
|
-
rescue StandardError => e
|
802
|
-
w_r_info("Error: #{e.inspect}")
|
803
|
-
end
|
804
|
-
hashtime = DateTime.now.strftime "%Y-%m-%d %H:%M"
|
805
|
-
if @hash.include?(Dir.pwd)
|
806
|
-
if @hash[Dir.pwd][1] == hashdir
|
807
|
-
w_b_info(" Hash for #{Dir.pwd} has NOT changed since #{hashtime} (#{hashdir})")
|
808
|
-
else
|
809
|
-
w_b_info(" Hash for #{Dir.pwd} has CHANGED since #{hashtime} (#{@hash[Dir.pwd][1]} -> #{hashdir})")
|
810
|
-
@hash[Dir.pwd] = [hashtime, hashdir]
|
811
|
-
end
|
812
|
-
else
|
813
|
-
hashtime = DateTime.now.strftime "%Y-%m-%d %H:%M"
|
814
|
-
@hash[Dir.pwd] = [hashtime, hashdir]
|
815
|
-
w_b_info(" New hash for #{Dir.pwd}: #{hashtime}: #{hashdir}")
|
816
|
-
end
|
817
|
-
@w_r.update = true
|
818
|
-
@w_b.update = false
|
819
|
-
when 'I' # OpenAI integration
|
820
|
-
if @ai
|
821
|
-
openai
|
822
|
-
else
|
823
|
-
w_b_info("No OpenAI key in config file. Add `@ai = 'your-secret-openai-key'` to .rtfm.conf")
|
824
|
-
end
|
825
|
-
# RIGHT PANE
|
826
|
-
when 'ENTER' # Refresh right pane
|
827
|
-
@w_r.fill # First clear the window, then clear any previously showing image
|
828
|
-
image_show("clear") if @image; @image = false
|
829
|
-
@w_r.update = true
|
830
|
-
@w_b.update = true
|
831
|
-
when 'TAB' # Start paging/Down one page
|
832
|
-
if @w_r.pager == 1 and @w_r.pager_cmd != ""
|
833
|
-
@w_r.text = `#{@w_r.pager_cmd} 2>/dev/null`
|
834
|
-
end
|
835
|
-
if @w_r.pager_more
|
836
|
-
@w_r.pager += 1
|
837
|
-
pager_show
|
838
|
-
end
|
839
|
-
@w_b.update = true
|
840
|
-
when 'S-TAB' # Up one page
|
841
|
-
if @w_r.pager > 1
|
842
|
-
@w_r.pager -= 1
|
843
|
-
pager_show
|
844
|
-
end
|
845
|
-
@w_b.update = true
|
846
|
-
when 'w' # Change width of left/right panes
|
847
|
-
@width += 1
|
848
|
-
@width = 2 if @width == 7
|
849
|
-
@break = true
|
850
|
-
@w_r.update = true
|
851
|
-
@w_b.update = true
|
852
|
-
when '-' # Toggle content view in right pane
|
853
|
-
@preview = !@preview
|
854
|
-
@preview ? p = "On" : p = "Off"
|
855
|
-
w_b_info("Preview = " + p)
|
856
|
-
getch
|
857
|
-
@break = true
|
858
|
-
when '_' # Toggle image view
|
859
|
-
@showimage = !@showimage
|
860
|
-
@showimage ? i = "On" : i = "Off"
|
861
|
-
w_b_info("Image preview = " + i)
|
862
|
-
getch
|
863
|
-
@break = true
|
864
|
-
when 'b' # Toggle syntax highlighting (via bat/batcat)
|
865
|
-
@batuse = !@batuse
|
866
|
-
@break = true
|
867
|
-
# ADDITIONAL COMMANDS
|
868
|
-
when 'f' # Filter out filetypes not matching @lsfiles
|
869
|
-
loop do
|
870
|
-
@lsfiles = w_b_getstr("Filetype(s) to show: ", @lsfiles)
|
871
|
-
break if @lsfiles.match(/^$|\w+(,\w+)*$/)
|
872
|
-
end
|
873
|
-
w_b_info(nil)
|
874
|
-
when 'F' # Filter out files not matching @lsmatch
|
875
|
-
@lsmatch = w_b_getstr("Files will match RegEx: ", @lsmatch)
|
876
|
-
w_b_info(nil)
|
877
|
-
when 'B' # Toggle borders
|
878
|
-
@border = !@border
|
879
|
-
@break = true
|
880
|
-
when ':' # Enter "command mode" in the bottom window - tries to execute the given command
|
881
|
-
@w_r.nohistory = false
|
882
|
-
cmd = w_b_getstr(": ", "")
|
883
|
-
w_b_exec(cmd)
|
884
|
-
@w_b.update = true
|
885
|
-
when ';' # Show command history
|
886
|
-
w_r_info("Command history (latest on top):\n\n" + @history.join("\n"))
|
887
|
-
@w_b.update = true
|
888
|
-
when 'y', 'Y' # Copy path of selected item
|
889
|
-
if @selected == nil
|
890
|
-
w_b_info(" No selected item path to copy")
|
891
|
-
else
|
892
|
-
path = Dir.pwd + "/" + @selected
|
893
|
-
if chr == 'Y'
|
894
|
-
clip = "xclip -selection clipboard"
|
895
|
-
w_b_info(" Path copied to clipboard")
|
896
|
-
else
|
897
|
-
clip = "xclip"
|
898
|
-
w_b_info(" Path copied to primary selection (paste with middle mouse button)")
|
1663
|
+
count = Dir.children(@selected).count
|
1664
|
+
text += " [#{count} items]"
|
1665
|
+
rescue Errno::EACCES
|
1666
|
+
text += ' [Denied]'
|
899
1667
|
end
|
900
|
-
system("echo -n '#{path}' | #{clip}")
|
901
1668
|
end
|
902
|
-
|
903
|
-
|
904
|
-
@
|
905
|
-
@cont.gsub!(/^¤¤\d+¤¤\d*¤¤ */, '')
|
906
|
-
@cont.gsub!(/¤¤\d+¤¤\d*¤¤/, '')
|
907
|
-
@cont.gsub!(/ /, '')
|
908
|
-
@cont.gsub!(/ (\d)/, '\1')
|
909
|
-
@cont.gsub!(/\[(\d+;)+\d+m/, '')
|
910
|
-
@cont = @cont.inspect
|
911
|
-
@cont.gsub!('\"', '"')
|
912
|
-
@cont = @cont [1...-1]
|
913
|
-
w_b_info(" Right pane copied to clipboard")
|
914
|
-
system("echo -n '#{@cont}' | #{clip}")
|
915
|
-
when 'S' # Show comprehensive system info
|
916
|
-
sysinfo
|
917
|
-
when 'C-D' # Create new directory (shortcut for ":mkdir ")
|
918
|
-
cmd = w_b_getstr(": ", "mkdir ")
|
919
|
-
w_b_exec(cmd + " -p")
|
920
|
-
when 'C-N' # navi integration (https://github.com/denisidoro/navi)
|
921
|
-
begin
|
922
|
-
@navi = `navi`
|
923
|
-
rescue
|
924
|
-
w_b_info(" navi not installed - see https://github.com/denisidoro/navi")
|
925
|
-
end
|
926
|
-
@break = true
|
927
|
-
when '@' # Enter "Ruby debug"
|
928
|
-
@w_b.nohistory = false
|
929
|
-
cmd = w_b_getstr("◆ ", "", true)
|
930
|
-
@w_b.clr
|
931
|
-
@w_b.refresh
|
932
|
-
@w_b.update = true
|
933
|
-
@w_r.clr
|
934
|
-
w_r_info("Command: #{cmd}\n")
|
935
|
-
begin
|
936
|
-
eval(cmd).to_s
|
937
|
-
rescue Exception => err
|
938
|
-
w_r_info("Error: #{err.inspect}")
|
939
|
-
end
|
940
|
-
@w_r.update = false
|
1669
|
+
@pT.text = text.b
|
1670
|
+
@pT.bg = @topmatch.find { |name, _| name.empty? || Dir.pwd.include?(name) }&.last
|
1671
|
+
@pT.refresh unless @pT.text == toptext
|
941
1672
|
end
|
942
|
-
|
943
|
-
|
944
|
-
|
945
|
-
|
946
|
-
|
947
|
-
|
948
|
-
|
949
|
-
|
1673
|
+
|
1674
|
+
# BOTTOM pane {{{3
|
1675
|
+
if @pB.update
|
1676
|
+
bottomtext = @pB.text
|
1677
|
+
info = ': for command (use @s for selected item, @t for tagged items) - press ? for help'
|
1678
|
+
info = " Showing only files matching '#{@lsmatch}'".fg(130).u if @lsmatch != ''
|
1679
|
+
info = " Showing only file type '#{@lsfiles}'".fg(129).u if @lsfiles != ''
|
1680
|
+
info = " Showing only file types '#{@lsfiles}'".fg(129).u if @lsfiles =~ /,/
|
1681
|
+
info += " and only files matching '#{@lsmatch}'".fg(129).u if @lsfiles != '' && @lsmatch != ''
|
1682
|
+
@pB.text = info
|
1683
|
+
@pB.refresh unless @pB.text == bottomtext
|
950
1684
|
end
|
951
1685
|
end
|
952
|
-
|
953
|
-
|
954
|
-
|
955
|
-
|
956
|
-
|
957
|
-
|
958
|
-
|
959
|
-
|
960
|
-
|
961
|
-
|
962
|
-
|
963
|
-
|
964
|
-
|
965
|
-
|
966
|
-
|
967
|
-
|
968
|
-
|
969
|
-
|
970
|
-
|
971
|
-
|
972
|
-
|
973
|
-
conf.sub!(/^@lsorder.*\n/, "")
|
974
|
-
conf += "@lsorder = \"#{@lsorder}\"\n"
|
975
|
-
conf.sub!(/^@lsinvert.*\n/, "")
|
976
|
-
conf += "@lsinvert = \"#{@lsinvert}\"\n"
|
977
|
-
conf.sub!(/^@width.*\n/, "")
|
978
|
-
conf += "@width = #{@width}\n"
|
979
|
-
conf.sub!(/^@border.*\n/, "")
|
980
|
-
conf += "@border = #{@border}\n"
|
981
|
-
conf.sub!(/^@preview.*\n/, "")
|
982
|
-
conf += "@preview = #{@preview}\n"
|
983
|
-
conf.sub!(/^@showimage.*\n/, "")
|
984
|
-
conf += "@showimage = #{@showimage}\n"
|
985
|
-
@w_r.fg = 249
|
986
|
-
w_r_info("Configuration written to .rtfm.conf:\n\n" + conf)
|
987
|
-
end
|
988
|
-
File.write(Dir.home+'/.rtfm.conf', conf)
|
989
|
-
end
|
990
|
-
def ansifix(text) # Format [[fg, attr, text]]
|
991
|
-
output = ""
|
992
|
-
text.each do |e|
|
993
|
-
ansi = "\e[38;5;#{e[0]}"
|
994
|
-
ansi += ";1" if e[1] == 1
|
995
|
-
ansi += "m"
|
996
|
-
output += ansi + e[2].gsub(/\n/, "\n#{ansi}")
|
997
|
-
end
|
998
|
-
return output
|
999
|
-
end
|
1000
|
-
def mark_latest
|
1001
|
-
@marks["5"] = @marks["4"]
|
1002
|
-
@marks["4"] = @marks["3"]
|
1003
|
-
@marks["3"] = @marks["2"]
|
1004
|
-
@marks["2"] = @marks["1"]
|
1005
|
-
@marks["1"] = @marks["'"]
|
1006
|
-
@marks["'"] = Dir.pwd
|
1686
|
+
|
1687
|
+
def refresh # {{{2
|
1688
|
+
@p0.w = @w
|
1689
|
+
@p0.h = @h
|
1690
|
+
@p0.clear
|
1691
|
+
@pT.w = @w
|
1692
|
+
@pT.clear; @pT.update = true
|
1693
|
+
@pB.w = @w
|
1694
|
+
@pB.y = @h
|
1695
|
+
@pB.clear; @pB.update = true
|
1696
|
+
@pL.w = (@w - 4) * @width / 10
|
1697
|
+
@pL.h = @h - 4
|
1698
|
+
@pR.x = @pL.w + 4
|
1699
|
+
@pR.w = @w - @pL.w - 4
|
1700
|
+
@pR.h = @h - 4
|
1701
|
+
@pL.clear; @pL.update = true
|
1702
|
+
@pR.clear; @pR.update = true
|
1703
|
+
@pCmd.y = @h
|
1704
|
+
@pCmd.w = @w
|
1705
|
+
@pRuby.y = @h
|
1706
|
+
@pRuby.w = @w
|
1007
1707
|
end
|
1008
|
-
|
1009
|
-
|
1010
|
-
|
1011
|
-
|
1012
|
-
|
1013
|
-
|
1014
|
-
|
1015
|
-
|
1016
|
-
|
1017
|
-
|
1018
|
-
|
1019
|
-
|
1020
|
-
|
1021
|
-
|
1022
|
-
|
1023
|
-
@files = @files - dirs
|
1024
|
-
@fspes = @fspes - dirs
|
1025
|
-
@files.select! {|f| lsf.any? {|l| File.extname(f) == ".#{l}"}} if @lsfiles != ""
|
1026
|
-
@fspes.select! {|f| lsf.any? {|l| File.extname(f) == ".#{l}"}} if @lsfiles != ""
|
1027
|
-
@files.select! {|f| f =~ /#{@lsmatch}/} if @lsmatch != ""
|
1028
|
-
@fspes.select! {|f| f =~ /#{@lsmatch}/} if @lsmatch != ""
|
1029
|
-
@files = dirs + @files
|
1030
|
-
@fspes = dirs + @fspes
|
1031
|
-
@w_r.update = true
|
1032
|
-
end
|
1033
|
-
end
|
1034
|
-
# TOP WINDOW FUNCTIONS
|
1035
|
-
def w_t_info # SHOW INFO IN @w_t
|
1036
|
-
text = " " + ENV['USER'].to_s + "@" + `hostname 2>/dev/null`.to_s.chop + ": " + Dir.pwd + "/"
|
1037
|
-
unless @selected == nil
|
1038
|
-
text += @selected
|
1039
|
-
text += " → #{File.readlink(@selected)}" if File.symlink?(@selected)
|
1040
|
-
end
|
1041
|
-
begin
|
1042
|
-
text += " (#{@fspes[@index]})"
|
1043
|
-
rescue
|
1044
|
-
end
|
1045
|
-
begin
|
1046
|
-
if @selected.match(@imagefile)
|
1047
|
-
text += `identify #{@selected_safe} | awk '{printf " [%s %s %s %s] ", $3,$2,$5,$6}' 2>/dev/null` if cmd?('identify')
|
1048
|
-
elsif @selected.match(@pdffile)
|
1049
|
-
info = `pdfinfo #{@selected_safe} 2>/dev/null`
|
1050
|
-
text += " [" + info.match(/Pages:.*?(\d+)/)[1]
|
1051
|
-
text += " " + info.match(/Page size:.*\((.*)\)/)[1] + " pages] "
|
1052
|
-
end
|
1053
|
-
rescue
|
1708
|
+
|
1709
|
+
def setborder # {{{2
|
1710
|
+
case @border
|
1711
|
+
when 0
|
1712
|
+
@pL.border = false
|
1713
|
+
@pR.border = false
|
1714
|
+
when 1
|
1715
|
+
@pL.border = false
|
1716
|
+
@pR.border = true
|
1717
|
+
when 2
|
1718
|
+
@pL.border = true
|
1719
|
+
@pR.border = true
|
1720
|
+
when 3
|
1721
|
+
@pL.border = true
|
1722
|
+
@pR.border = false
|
1054
1723
|
end
|
1055
|
-
|
1056
|
-
|
1057
|
-
|
1058
|
-
|
1059
|
-
|
1724
|
+
refresh
|
1725
|
+
end
|
1726
|
+
|
1727
|
+
def errormsg(msg, err) # {{{2
|
1728
|
+
text = "#{msg}: #{err.class} – #{err.message}\n\n".fg(196)
|
1729
|
+
err.backtrace.each { |ln| text += " #{ln}\n" } # Full backtrace
|
1730
|
+
text += "\nPlease report error as an RTFM issue: https://github.com/isene/RTFM/issues"
|
1731
|
+
@pR.say(text)
|
1732
|
+
end
|
1733
|
+
|
1734
|
+
def shell(cmd, background: false, err: nil) # {{{2
|
1735
|
+
# if caller passed err: filename, use that, otherwise fall back to the default log in /tmp
|
1736
|
+
tmp = err || File.join(Dir.tmpdir, 'rtfm_err.log')
|
1737
|
+
# build the full command, redirecting stderr to tmp
|
1738
|
+
full = if background
|
1739
|
+
"#{cmd} 2>#{tmp} &"
|
1740
|
+
else
|
1741
|
+
"#{cmd} 2>#{tmp}"
|
1742
|
+
end
|
1743
|
+
system(full)
|
1744
|
+
if File.exist?(tmp) # if anything was written, show it in @pR
|
1745
|
+
sleep 0.1
|
1746
|
+
err_text = File.read(tmp)
|
1747
|
+
unless err_text.strip.empty?
|
1748
|
+
@pR.say(err_text.fg(196))
|
1749
|
+
@pR.update = false
|
1060
1750
|
end
|
1751
|
+
File.delete(tmp)
|
1061
1752
|
end
|
1062
|
-
|
1063
|
-
|
1064
|
-
|
1065
|
-
|
1066
|
-
|
1067
|
-
|
1068
|
-
|
1069
|
-
|
1070
|
-
|
1071
|
-
|
1072
|
-
|
1073
|
-
|
1074
|
-
|
1075
|
-
|
1076
|
-
|
1077
|
-
|
1078
|
-
|
1079
|
-
|
1080
|
-
|
1081
|
-
|
1082
|
-
|
1083
|
-
|
1084
|
-
|
1085
|
-
|
1753
|
+
nil
|
1754
|
+
end
|
1755
|
+
|
1756
|
+
def shellexec(cmd, timeout: 10) # {{{2
|
1757
|
+
out, err = command(cmd, timeout: timeout, return_both: true)
|
1758
|
+
@pR.say(err.fg(196)) unless err.empty?
|
1759
|
+
@pR.say(out) unless out.empty?
|
1760
|
+
@pB.full_refresh
|
1761
|
+
end
|
1762
|
+
|
1763
|
+
def command(cmd, timeout: 5, return_both: false) # {{{2
|
1764
|
+
# Run a shell command with timeout, drain pipes concurrently,
|
1765
|
+
# and optionally return both stdout+stderr instead of auto-saying stderr
|
1766
|
+
#
|
1767
|
+
# @param cmd [String] the command to run via `bash -c`
|
1768
|
+
# @param timeout [Numeric, nil] seconds to wait (nil = wait forever)
|
1769
|
+
# @param return_both [Boolean] if true, return [stdout, stderr] instead of auto-saying
|
1770
|
+
# @return [String] stdout (when return_both: false)
|
1771
|
+
# @return [Array<String,String>] [stdout, stderr] (when return_both: true)
|
1772
|
+
cmd_array = ['bash', '-c', cmd]
|
1773
|
+
out_buf = String.new
|
1774
|
+
err_buf = String.new
|
1775
|
+
begin
|
1776
|
+
Open3.popen3(*cmd_array) do |_stdin, stdout, stderr, wait_thr|
|
1777
|
+
pid = wait_thr.pid
|
1778
|
+
# Drain both pipes in background threads
|
1779
|
+
out_reader = Thread.new { out_buf << stdout.read until stdout.eof? }
|
1780
|
+
err_reader = Thread.new { err_buf << stderr.read until stderr.eof? }
|
1781
|
+
if timeout
|
1782
|
+
unless wait_thr.join(timeout)
|
1783
|
+
# Timed out → kill everything
|
1784
|
+
Process.kill('TERM', pid) rescue nil
|
1785
|
+
sleep 0.1
|
1786
|
+
Process.kill('KILL', pid) rescue nil
|
1787
|
+
Process.wait(pid) rescue nil
|
1788
|
+
out_reader.kill
|
1789
|
+
err_reader.kill
|
1790
|
+
showimage('clear') if @image
|
1791
|
+
@pR.say('Error: Command timed out.'.fg(196))
|
1792
|
+
return return_both ? ['', "Error: Command timed out.\n"] : ''
|
1793
|
+
end
|
1794
|
+
else
|
1795
|
+
wait_thr.join
|
1086
1796
|
end
|
1087
|
-
|
1797
|
+
# Ensure we've captured all output
|
1798
|
+
out_reader.join
|
1799
|
+
err_reader.join
|
1088
1800
|
end
|
1089
|
-
|
1090
|
-
|
1091
|
-
fg, bold = fl[0][0].to_i, fl[0][1].to_i unless fl[0] == nil
|
1092
|
-
init_pair(fg, fg, bg)
|
1093
|
-
file_marker = color_pair(fg)
|
1094
|
-
file_marker = file_marker | Curses::A_BOLD if bold == 1
|
1095
|
-
if ix == @index and active
|
1096
|
-
@w_l << "→ "
|
1097
|
-
file_marker = file_marker | Curses::A_UNDERLINE
|
1098
|
-
wixy = win.cury
|
1801
|
+
if return_both
|
1802
|
+
[out_buf, err_buf]
|
1099
1803
|
else
|
1100
|
-
|
1804
|
+
@pR.say(err_buf.fg(196)) unless err_buf.empty?
|
1805
|
+
out_buf
|
1101
1806
|
end
|
1102
|
-
|
1103
|
-
|
1104
|
-
|
1105
|
-
|
1106
|
-
|
1107
|
-
|
1108
|
-
|
1109
|
-
|
1110
|
-
ext_name = File.extname(str)
|
1111
|
-
ext_length = ext_name.length
|
1112
|
-
nbl = win.maxx - 6 - ext_length # nbl: new_base_length
|
1113
|
-
nbl -= 1 if dir != "" # Account for one extra character
|
1114
|
-
nbl -= 1 if link != "" # Account for one extra character
|
1115
|
-
str = base_name[0..nbl] + "…" + ext_name
|
1116
|
-
end
|
1117
|
-
if !active and ix == win.maxy - 1 # Add indicator of more at bottom @w_r list
|
1118
|
-
win << " ..."
|
1119
|
-
return
|
1120
|
-
end
|
1121
|
-
str += link + dir
|
1122
|
-
@cont += str + "\n" unless active # Adds to content in right win for copying
|
1123
|
-
win.attron(file_marker) { win << str } # Implement color/bold to the item
|
1124
|
-
win.clrtoeol
|
1125
|
-
win << "\n"
|
1126
|
-
ix += 1; t += 1
|
1127
|
-
end
|
1128
|
-
(win.maxy - win.cury).times {win.deleteln()} # Clear to bottom of window
|
1129
|
-
if active
|
1130
|
-
init_pair(242, 242, 0)
|
1131
|
-
if @index > @w_l.maxy/2
|
1132
|
-
@w_l.setpos(0, @w_l.maxx - 1)
|
1133
|
-
@w_l.attron(color_pair(242) | Curses::A_DIM) { @w_l << "∆" }
|
1134
|
-
end
|
1135
|
-
if @files.length > @w_l.maxy - 1 and @files.length > @index + @w_l.maxy/2 - 1
|
1136
|
-
@w_l.setpos(@w_l.maxy - 2, @w_l.maxx - 1)
|
1137
|
-
@w_l.attron(color_pair(242) | Curses::A_DIM) { @w_l << "∇" }
|
1138
|
-
end
|
1139
|
-
end
|
1140
|
-
end
|
1141
|
-
def open_selected(html = nil) # OPEN SELECTED ITEM (when pressing RIGHT)
|
1142
|
-
if File.directory?(@selected) # Rescue for permission error
|
1143
|
-
begin
|
1144
|
-
mark_latest
|
1145
|
-
Dir.chdir(@selected)
|
1146
|
-
rescue
|
1807
|
+
rescue IOError => e
|
1808
|
+
msg = "Error: Stream closed – #{e.message}\n"
|
1809
|
+
if return_both
|
1810
|
+
['', msg]
|
1811
|
+
else
|
1812
|
+
showimage('clear') if @image
|
1813
|
+
@pR.say(msg.fg(196))
|
1814
|
+
''
|
1147
1815
|
end
|
1148
|
-
|
1149
|
-
|
1150
|
-
|
1151
|
-
|
1152
|
-
|
1153
|
-
|
1154
|
-
|
1155
|
-
else
|
1156
|
-
system("xdg-open #{@selected_safe} 2>/dev/null &")
|
1157
|
-
end
|
1158
|
-
end
|
1159
|
-
@break = true
|
1160
|
-
rescue
|
1816
|
+
rescue => e
|
1817
|
+
msg = "Error: #{e.message}\n#{e.backtrace.join("\n")}\n"
|
1818
|
+
if return_both
|
1819
|
+
['', msg]
|
1820
|
+
else
|
1821
|
+
@pR.say(msg.fg(196))
|
1822
|
+
''
|
1161
1823
|
end
|
1162
1824
|
end
|
1163
1825
|
end
|
1164
|
-
|
1826
|
+
|
1827
|
+
def copy_move_link(type) # COPY OR MOVE TAGGED ITEMS {{{2
|
1165
1828
|
@tagged.uniq!
|
1166
|
-
@tagged.each do |
|
1167
|
-
|
1168
|
-
dest
|
1169
|
-
dest += "/" + File.basename(item)
|
1170
|
-
dest += "1" if File.exist?(dest)
|
1829
|
+
@tagged.each do |item|
|
1830
|
+
dest = File.join(Dir.pwd, File.basename(item))
|
1831
|
+
dest += '1' if File.exist?(dest)
|
1171
1832
|
while File.exist?(dest)
|
1833
|
+
# Replace the last character (presumed to be a digit) by incrementing it
|
1172
1834
|
dest = dest.chop + (dest[-1].to_i + 1).to_s
|
1173
1835
|
end
|
1174
1836
|
begin
|
1175
1837
|
case type
|
1176
|
-
when
|
1838
|
+
when 'copy'
|
1177
1839
|
FileUtils.cp_r(item, dest)
|
1178
|
-
|
1179
|
-
when
|
1840
|
+
@pB.say(' Item(s) copied here.')
|
1841
|
+
when 'move'
|
1180
1842
|
FileUtils.mv(item, dest)
|
1181
|
-
|
1182
|
-
when
|
1843
|
+
@pB.say(' Item(s) moved here.')
|
1844
|
+
when 'link'
|
1183
1845
|
FileUtils.ln_s(item, dest)
|
1184
|
-
|
1846
|
+
@pB.say(' Item(s) symlinked here.')
|
1185
1847
|
end
|
1186
|
-
rescue
|
1187
|
-
|
1848
|
+
rescue => e
|
1849
|
+
@pB.say(e.to_s)
|
1188
1850
|
end
|
1189
1851
|
end
|
1190
1852
|
@tagged = []
|
1853
|
+
render
|
1191
1854
|
end
|
1192
|
-
|
1193
|
-
def
|
1194
|
-
|
1195
|
-
|
1196
|
-
|
1197
|
-
|
1198
|
-
|
1199
|
-
|
1200
|
-
|
1201
|
-
|
1202
|
-
|
1203
|
-
|
1204
|
-
|
1205
|
-
|
1206
|
-
|
1207
|
-
|
1208
|
-
|
1209
|
-
|
1210
|
-
|
1211
|
-
|
1212
|
-
|
1213
|
-
|
1214
|
-
|
1215
|
-
|
1216
|
-
|
1217
|
-
|
1218
|
-
|
1219
|
-
|
1220
|
-
|
1221
|
-
|
1222
|
-
|
1223
|
-
|
1224
|
-
|
1225
|
-
|
1226
|
-
|
1227
|
-
|
1228
|
-
|
1229
|
-
|
1230
|
-
|
1231
|
-
elsif @selected.match(@docxfile) and @w_r.pager == 0
|
1232
|
-
@w_r.pager_cmd = "docx2txt #{@selected_safe} - 2>/dev/null"
|
1233
|
-
w_r_doc
|
1234
|
-
# MS XLSX
|
1235
|
-
elsif @selected.match(@xlsxfile) and @w_r.pager == 0
|
1236
|
-
@w_r.pager_cmd = "ssconvert -O 'separator= ' -T Gnumeric_stf:stf_assistant #{@selected_safe} fd://1 2>/dev/null"
|
1237
|
-
w_r_doc
|
1238
|
-
# MS PPTX
|
1239
|
-
elsif @selected.match(@pptxfile) and @w_r.pager == 0
|
1240
|
-
@w_r.pager_cmd = %Q[unzip -qc #{@selected_safe} | ruby -e '$stdin.each_line { |i| i.force_encoding("ISO-8859-1").scan(/<a:t>(.+?)<\\/a:t>/).each { |j| puts(j) } }' 2>/dev/null]
|
1241
|
-
w_r_doc
|
1242
|
-
# MS DOC
|
1243
|
-
elsif @selected.match(@docfile) and @w_r.pager == 0
|
1244
|
-
@w_r.pager_cmd = "catdoc #{@selected_safe} 2>/dev/null"
|
1245
|
-
w_r_doc
|
1246
|
-
# MS XLS
|
1247
|
-
elsif @selected.match(@xlsfile) and @w_r.pager == 0
|
1248
|
-
@w_r.pager_cmd = "xls2csv #{@selected_safe} 2>/dev/null"
|
1249
|
-
w_r_doc
|
1250
|
-
# MS PPT
|
1251
|
-
elsif @selected.match(@pptfile) and @w_r.pager == 0
|
1252
|
-
@w_r.pager_cmd = "catppt #{@selected_safe} 2>/dev/null"
|
1253
|
-
w_r_doc
|
1254
|
-
# IMAGES
|
1255
|
-
elsif @selected.match(@imagefile)
|
1256
|
-
image_show(@selected_safe)
|
1257
|
-
@image = true
|
1258
|
-
# VIDEOS (THUMBNAILS)
|
1259
|
-
elsif @selected.match(/\.mpg$|\.mpeg$|\.avi$|\.mov$|\.mkv$|\.mp4$/)
|
1260
|
-
begin
|
1261
|
-
tmpfile = "/tmp/" + File.basename(@selected_safe,".*")
|
1262
|
-
`ffmpegthumbnailer -s 1200 -i #{@selected_safe} -o /tmp/rtfm_video_tn.jpg 2>/dev/null`
|
1263
|
-
image_show("/tmp/rtfm_video_tn.jpg")
|
1264
|
-
@image = true
|
1265
|
-
rescue
|
1266
|
-
end
|
1855
|
+
|
1856
|
+
def mark_latest # UPDATE MARKS LIST {{{2
|
1857
|
+
@marks['5'] = @marks['4']
|
1858
|
+
@marks['4'] = @marks['3']
|
1859
|
+
@marks['3'] = @marks['2']
|
1860
|
+
@marks['2'] = @marks['1']
|
1861
|
+
@marks['1'] = @marks["'"]
|
1862
|
+
@marks["'"] = Dir.pwd
|
1863
|
+
end
|
1864
|
+
|
1865
|
+
def conf_write(all: false) # WRITE TO ~/.rtfm/conf {{{2
|
1866
|
+
@conf = @conf.dup # work on a mutable copy
|
1867
|
+
assignments = {
|
1868
|
+
'marks' => "@marks = #{@marks}",
|
1869
|
+
'hash' => "@hash = #{@hash}",
|
1870
|
+
'history' => "@history = #{@pCmd.history.reverse.uniq.reverse.last(40)}",
|
1871
|
+
'rubyhistory' => "@rubyhistory = #{@pRuby.history.reverse.uniq.reverse.last(40)}",
|
1872
|
+
'aihistory' => "@aihistory = #{@pAI.history.reverse.uniq.reverse.last(40)}"
|
1873
|
+
}
|
1874
|
+
if all
|
1875
|
+
assignments.merge!(
|
1876
|
+
'lslong' => "@lslong = '#{@lslong}'",
|
1877
|
+
'lsall' => "@lsall = '#{@lsall}'",
|
1878
|
+
'lsorder' => "@lsorder = '#{@lsorder}'",
|
1879
|
+
'lsinvert' => "@lsinvert = '#{@lsinvert}'",
|
1880
|
+
'width' => "@width = #{@width}",
|
1881
|
+
'border' => "@border = #{@border}",
|
1882
|
+
'preview' => "@preview = #{@preview}",
|
1883
|
+
'trash' => "@trash = #{@trash}",
|
1884
|
+
'interactive' => "@interactive = '#{@interactive}'"
|
1885
|
+
)
|
1886
|
+
end
|
1887
|
+
assignments.each do |var, line| # For each var, replace its line or append if missing
|
1888
|
+
regex = /^@#{Regexp.escape(var)}\b.*$/
|
1889
|
+
if @conf.match?(regex)
|
1890
|
+
@conf.sub!(regex, line)
|
1891
|
+
else
|
1892
|
+
@conf << "\n" unless @conf.end_with?("\n")
|
1893
|
+
@conf << line << "\n"
|
1267
1894
|
end
|
1268
|
-
rescue
|
1269
1895
|
end
|
1270
|
-
|
1271
|
-
@
|
1272
|
-
@
|
1896
|
+
show_config
|
1897
|
+
@pB.say('Configuration written to ~/.rtfm/conf')
|
1898
|
+
File.write(CONFIG_FILE, @conf)
|
1273
1899
|
end
|
1274
|
-
|
1275
|
-
|
1276
|
-
|
1277
|
-
|
1278
|
-
|
1279
|
-
|
1900
|
+
|
1901
|
+
def exit_rtfm # CLEAN EXIT {{{2
|
1902
|
+
# If invoked with a stub filename, write out our cwd
|
1903
|
+
if ARGV[0]
|
1904
|
+
begin
|
1905
|
+
File.write(ARGV[0], Dir.pwd)
|
1906
|
+
rescue StandardError # rubocop:disable Lint/SuppressedException
|
1907
|
+
end
|
1908
|
+
end
|
1909
|
+
# Restore terminal state
|
1910
|
+
$stdin.cooked!
|
1911
|
+
$stdin.echo = true
|
1912
|
+
Rcurses.clear_screen
|
1913
|
+
Cursor.show
|
1914
|
+
# Also record for the 'r' launcher
|
1915
|
+
last = File.join(Dir.home, '.rtfm_last_dir')
|
1280
1916
|
begin
|
1281
|
-
|
1282
|
-
|
1283
|
-
|
1284
|
-
w_r_width = Curses.cols - (Curses.cols * @width / 10) - 2
|
1285
|
-
info.gsub!(/(.{1,#{w_r_width}})( +|$\n?)|(.{1,#{w_r_width}})/, "\\1\\3\n")
|
1286
|
-
@w_r.text = info
|
1287
|
-
@w_r.pager_cmd = ""
|
1288
|
-
pager_start
|
1289
|
-
pager_show
|
1290
|
-
@w_r.update = false
|
1291
|
-
rescue
|
1917
|
+
File.write(last, Dir.pwd)
|
1918
|
+
rescue StandardError
|
1919
|
+
# ignore write errors
|
1292
1920
|
end
|
1921
|
+
exit(0)
|
1293
1922
|
end
|
1294
|
-
|
1295
|
-
|
1296
|
-
|
1297
|
-
|
1298
|
-
|
1299
|
-
|
1300
|
-
|
1923
|
+
|
1924
|
+
def log(msg) # LOG TO LOGFILE {{{2
|
1925
|
+
File.write(LOG_PATH, "#{Time.now.iso8601} #{msg}\n", mode: 'a')
|
1926
|
+
end
|
1927
|
+
|
1928
|
+
# RIGHT PANE FUNCTIONS {{{1
|
1929
|
+
def showcommand(cmd) # Helper function for showcontent {{{2
|
1930
|
+
c = command(cmd)
|
1931
|
+
return if c == ''
|
1932
|
+
|
1933
|
+
@pR.say(c)
|
1934
|
+
end
|
1935
|
+
|
1936
|
+
def showcontent # SHOW CONTENTS IN THE RIGHT WINDOW {{{2
|
1937
|
+
if @pR.update
|
1938
|
+
showimage('clear') if @image
|
1939
|
+
@image = false
|
1940
|
+
@pR.clear
|
1941
|
+
end
|
1942
|
+
begin
|
1943
|
+
if @selected && File.directory?(@selected)
|
1944
|
+
@pR.say(dirlist(left: false))
|
1945
|
+
else # Look up first matching handler
|
1946
|
+
entry = PREVIEW_HANDLERS.find { |re, _| re.match?(@selected) }
|
1947
|
+
pattern, tmpl = entry || [nil, nil]
|
1948
|
+
if tmpl # Command-based preview
|
1949
|
+
escaped = Shellwords.escape(@selected)
|
1950
|
+
if !@batuse && tmpl.match?(/\b#{@bat}\b/)
|
1951
|
+
showcommand("cat #{escaped}")
|
1952
|
+
else
|
1953
|
+
cmd = tmpl.gsub('@s', escaped)
|
1954
|
+
showcommand(cmd)
|
1955
|
+
end
|
1956
|
+
elsif pattern # Nil template → image or video
|
1957
|
+
case @selected
|
1958
|
+
when /\.(?:png|jpe?g|bmp|gif|webp|tiff?)$/i
|
1959
|
+
showimage(Shellwords.escape(@selected))
|
1960
|
+
@image = true
|
1961
|
+
when /\.(?:mpg|mpeg|avi|mov|mkv|mp4)$/i
|
1962
|
+
tn = '/tmp/rtfm_video_tn.jpg'
|
1963
|
+
showcommand("ffmpegthumbnailer -s 1200 -i #{Shellwords.escape(@selected)} -o #{Shellwords.escape(tn)} 2>/dev/null")
|
1964
|
+
showimage(tn)
|
1965
|
+
@image = true
|
1966
|
+
else
|
1967
|
+
@pR.say("No preview available for #{@selected}")
|
1968
|
+
end
|
1969
|
+
else # Fallback: treat as text if UTF‑8
|
1970
|
+
text = File.read(@selected).force_encoding('UTF-8') rescue ''
|
1971
|
+
if text.valid_encoding?
|
1972
|
+
if @batuse
|
1973
|
+
begin
|
1974
|
+
showcommand("#{@bat} -n --color=always #{Shellwords.escape(@selected)}")
|
1975
|
+
rescue
|
1976
|
+
showcommand("cat #{Shellwords.escape(@selected)}")
|
1977
|
+
end
|
1978
|
+
else
|
1979
|
+
showcommand("cat #{Shellwords.escape(@selected)}")
|
1980
|
+
end
|
1981
|
+
else
|
1982
|
+
@pR.say("No preview available for #{@selected}")
|
1983
|
+
end
|
1984
|
+
end
|
1301
1985
|
end
|
1302
|
-
|
1303
|
-
|
1986
|
+
rescue StandardError => e
|
1987
|
+
errormsg("⚠ Error while previewing #{@selected}", e)
|
1304
1988
|
end
|
1305
|
-
|
1306
|
-
end
|
1307
|
-
def tagged_info # SHOW THE LIST OF TAGGED ITEMS IN @w_r
|
1308
|
-
info = "TAGGED: (#{@tagged.size} items)\n"
|
1309
|
-
@tagged.empty? ? info += "(None)" : info += @tagged.join("\n")
|
1310
|
-
w_r_info(info)
|
1989
|
+
@pR.update = false
|
1311
1990
|
end
|
1312
|
-
|
1313
|
-
|
1991
|
+
|
1992
|
+
def showimage(image) # SHOW THE SELECTED IMAGE IN THE RIGHT WINDOW {{{2
|
1993
|
+
# Pass 'clear' to clear the window for previous image
|
1314
1994
|
return unless @showimage
|
1995
|
+
|
1315
1996
|
begin
|
1316
1997
|
terminfo = `xwininfo -id $(xdotool getactivewindow 2>/dev/null) 2>/dev/null`
|
1317
1998
|
term_w = terminfo.match(/Width: (\d+)/)[1].to_i
|
1318
1999
|
term_h = terminfo.match(/Height: (\d+)/)[1].to_i
|
1319
|
-
char_w = term_w /
|
1320
|
-
char_h = term_h /
|
1321
|
-
img_x = char_w *
|
2000
|
+
char_w = term_w / @w
|
2001
|
+
char_h = term_h / @h
|
2002
|
+
img_x = char_w * @pR.x
|
1322
2003
|
img_y = char_h * 2
|
1323
|
-
img_max_w = char_w * (
|
1324
|
-
img_max_h = char_h * (
|
1325
|
-
if image ==
|
2004
|
+
img_max_w = char_w * (@pR.w - 1)
|
2005
|
+
img_max_h = char_h * (@pR.h - 1)
|
2006
|
+
if image == 'clear'
|
1326
2007
|
`clear`
|
1327
2008
|
img_x -= char_w
|
1328
2009
|
img_max_w += char_w + 2
|
1329
2010
|
img_max_h += 2
|
1330
2011
|
`echo "6;#{img_x};#{img_y};#{img_max_w};#{img_max_h};\n4;\n3;" | #{@imgdisplay} 2>/dev/null`
|
1331
2012
|
else
|
1332
|
-
img_w,img_h = `identify -format "%[fx:w]x%[fx:h]" #{image} 2>/dev/null`.split('x')
|
1333
|
-
img_w
|
1334
|
-
img_h
|
2013
|
+
img_w, img_h = `identify -format "%[fx:w]x%[fx:h]" #{image} 2>/dev/null`.split('x')
|
2014
|
+
img_w = img_w.to_i
|
2015
|
+
img_h = img_h.to_i
|
1335
2016
|
if img_w > img_max_w
|
1336
2017
|
img_h = img_h * img_max_w / img_w
|
1337
2018
|
img_w = img_max_w
|
@@ -1343,433 +2024,158 @@ def image_show(image)# SHOW THE SELECTED IMAGE IN THE RIGHT WINDOW
|
|
1343
2024
|
`echo "0;1;#{img_x};#{img_y};#{img_w};#{img_h};;;;;\"#{image}\"\n4;\n3;" | #{@imgdisplay} 2>/dev/null`
|
1344
2025
|
end
|
1345
2026
|
rescue
|
1346
|
-
@
|
1347
|
-
@w_r << "Error showing image"
|
2027
|
+
@pR.text = 'Error showing image'
|
1348
2028
|
end
|
1349
2029
|
end
|
1350
|
-
|
1351
|
-
|
1352
|
-
|
1353
|
-
|
1354
|
-
|
1355
|
-
|
1356
|
-
end
|
1357
|
-
def pager_show # SHOW THE CURRENT PAGE CONTENT
|
1358
|
-
@w_r.setpos(0,0)
|
1359
|
-
beg_l = (@w_r.pager - 1) * (@w_r.maxy - 5)
|
1360
|
-
end_l = beg_l + @w_r.maxy - 2
|
1361
|
-
input = @w_r.text.lines[beg_l..end_l].join() + "\n"
|
1362
|
-
input.lines.count > @w_r.maxy - 2 ? @w_r.pager_more = true : @w_r.pager_more = false
|
1363
|
-
if @w_r.text.match(/^\e\[/)
|
1364
|
-
syntax_highlight(input)
|
2030
|
+
|
2031
|
+
def marks_info # SHOW MARKS IN RIGHT WINDOW {{{2
|
2032
|
+
@marks = @marks.sort.to_h
|
2033
|
+
info = ' ' + 'MARKS'.u + ":\n\n"
|
2034
|
+
if @marks.empty?
|
2035
|
+
info += ' (none)'
|
1365
2036
|
else
|
1366
|
-
|
1367
|
-
|
1368
|
-
|
1369
|
-
|
1370
|
-
init_pair(@w_r.fg, @w_r.fg, 0)
|
2037
|
+
@marks.each do |mark, dir|
|
2038
|
+
info += " #{mark} = #{dir}\n"
|
2039
|
+
info += "\n" if mark == "'"
|
2040
|
+
info += "\n" if mark == '5'
|
1371
2041
|
end
|
1372
|
-
@w_r.attr = 0 if @w_r.attr == nil
|
1373
|
-
@w_r.attron(color_pair(@w_r.fg) | @w_r.attr) { @w_r << input }
|
1374
2042
|
end
|
1375
|
-
@
|
1376
|
-
|
1377
|
-
|
1378
|
-
|
2043
|
+
@pR.say(info.fg(156))
|
2044
|
+
end
|
2045
|
+
|
2046
|
+
def tagged_info # SHOW THE LIST OF TAGGED ITEMS IN @pR {{{2
|
2047
|
+
info = ' ' + "TAGGED (#{@tagged.size} items, #{(@tagsize.to_f / 1_000_000).round(2)}MB)".u + ":\n\n"
|
2048
|
+
info += @tagged.empty? ? ' (None)' : ' ' + @tagged.join("\n ")
|
2049
|
+
info += "\n\n " + 'Selected'.u + ":\n " + @selected.b
|
2050
|
+
@pR.say(info.fg(204))
|
1379
2051
|
end
|
1380
|
-
|
1381
|
-
|
1382
|
-
|
1383
|
-
|
2052
|
+
|
2053
|
+
def open_selected(html = nil) # OPEN SELECTED ITEM (when pressing RIGHT or via open_force) {{{2
|
2054
|
+
require 'tmpdir' unless Dir.respond_to?(:tmpdir)
|
2055
|
+
if File.directory?(@selected) # Dir? just cd into it
|
2056
|
+
mark_latest
|
2057
|
+
Dir.chdir(@selected) rescue nil
|
2058
|
+
return
|
1384
2059
|
end
|
1385
|
-
|
1386
|
-
|
1387
|
-
|
2060
|
+
tmpfile = File.join(Dir.tmpdir, 'rtfm_err.log')
|
2061
|
+
paths = (@tagged + [@selected]).uniq
|
2062
|
+
if html # html mode - open in HTML-browser
|
2063
|
+
esc = paths.map { |p| Shellwords.escape(p) }.join(' ')
|
2064
|
+
shell("xdg-open #{esc} &", err: tmpfile)
|
2065
|
+
Rcurses.clear_screen; refresh; render
|
2066
|
+
if File.exist?(tmpfile)
|
2067
|
+
sleep 0.5
|
2068
|
+
err = File.read(tmpfile)
|
2069
|
+
showimage('clear') if @image
|
2070
|
+
@pR.say(err.fg(196))
|
2071
|
+
File.delete(tmpfile)
|
2072
|
+
end
|
2073
|
+
return
|
1388
2074
|
end
|
1389
|
-
|
1390
|
-
|
1391
|
-
|
1392
|
-
|
1393
|
-
|
1394
|
-
|
1395
|
-
end
|
1396
|
-
def openai # INTERFACE TO OPENAI
|
1397
|
-
begin
|
1398
|
-
require "ruby/openai"
|
1399
|
-
rescue
|
1400
|
-
w_b_info("To make openai work in RTFM, you need to do `gem install ruby-openai` and add this to your .rtfm.conf: $ai = 'your-secret-openai-key'")
|
2075
|
+
if File.read(@selected).force_encoding('UTF-8').valid_encoding? # Pure text
|
2076
|
+
system("exec $EDITOR #{Shellwords.escape(@selected)}")
|
2077
|
+
Rcurses.clear_screen
|
2078
|
+
refresh
|
2079
|
+
render
|
2080
|
+
return
|
1401
2081
|
end
|
1402
|
-
|
1403
|
-
|
2082
|
+
if @runmailcap # Open with run-mailcap or xdg-open
|
2083
|
+
arg = paths.map { |p| Shellwords.escape(p) }.join(' ')
|
2084
|
+
shell("run-mailcap #{arg} &", err: tmpfile)
|
2085
|
+
else
|
2086
|
+
shell("xdg-open #{Shellwords.escape(@selected)} &", err: tmpfile)
|
2087
|
+
end
|
2088
|
+
# Clean up
|
2089
|
+
Rcurses.clear_screen; refresh; render
|
2090
|
+
if File.exist?(tmpfile)
|
2091
|
+
sleep 0.5
|
2092
|
+
err = File.read(tmpfile)
|
2093
|
+
showimage('clear') if @image
|
2094
|
+
@pR.say(err.fg(196))
|
2095
|
+
File.delete(tmpfile)
|
2096
|
+
end
|
2097
|
+
end
|
1404
2098
|
|
1405
|
-
|
1406
|
-
|
1407
|
-
|
1408
|
-
w_r_info("OpenAI description...")
|
2099
|
+
# MAIN PROGRAM {{{1
|
2100
|
+
## Get terminal size {{{2
|
2101
|
+
@h, @w = IO.console.winsize
|
1409
2102
|
|
1410
|
-
|
1411
|
-
|
1412
|
-
|
1413
|
-
|
1414
|
-
|
2103
|
+
## Create panes {{{2
|
2104
|
+
# rubocop:disable Naming/VariableName
|
2105
|
+
# p = Pane.new( x, y, width, height, fg, bg)
|
2106
|
+
@p0 = Pane.new( 1, 1, @w, @h, 0, 0)
|
2107
|
+
@pT = Pane.new( 1, 1, @w, 1, 0, @topcolor)
|
2108
|
+
@pB = Pane.new( 1, @h, @w, 1, 252, @bottomcolor)
|
2109
|
+
@pL = Pane.new( 2, 3, (@w - 4)*@width/10, @h - 4, 15, 0)
|
2110
|
+
@pR = Pane.new(@pL.w + 4, 3, @w - @pL.w - 4, @h - 4, 255, 0)
|
2111
|
+
## Create special panes
|
2112
|
+
@pCmd = Pane.new( 1, @h, @w, 1, 255, @cmdcolor)
|
2113
|
+
@pSearch = Pane.new( 1, @h, @w, 1, 255, @searchcolor)
|
2114
|
+
@pRuby = Pane.new( 1, @h, @w, 1, 255, @rubycolor)
|
2115
|
+
@pAI = Pane.new( 1, @h, @w, 1, 255, @aicolor)
|
2116
|
+
# rubocop:enable Naming/VariableName
|
2117
|
+
#checkpoint("Panes created")
|
1415
2118
|
|
1416
|
-
|
1417
|
-
|
1418
|
-
|
1419
|
-
|
1420
|
-
|
1421
|
-
|
1422
|
-
|
1423
|
-
|
2119
|
+
## Set pane properties {{{2
|
2120
|
+
@pT.update = true
|
2121
|
+
@pL.update = true
|
2122
|
+
@pR.update = true
|
2123
|
+
@pB.update = true
|
2124
|
+
@pSearch.record = true
|
2125
|
+
@pCmd.record = true
|
2126
|
+
@pCmd.history = @history
|
2127
|
+
@pRuby.record = true
|
2128
|
+
@pRuby.history = @rubyhistory
|
2129
|
+
@pAI.record = true
|
2130
|
+
@pAI.history = @aihistory
|
2131
|
+
|
2132
|
+
# Report plugin errors {{{2
|
2133
|
+
@pR.say("Plugin load errors:\n" + @plugin_errors.join("\n").fg(196)) if @plugin_errors.any?
|
1424
2134
|
|
1425
|
-
|
1426
|
-
|
2135
|
+
## Set the borders {{{2
|
2136
|
+
setborder
|
2137
|
+
|
2138
|
+
## Catch change in terminal resize, redraw {{{2
|
2139
|
+
Signal.trap('WINCH') do
|
2140
|
+
@h, @w = IO.console.winsize
|
2141
|
+
@pT.update = @pL.update = @pR.update = @pB.update = true
|
2142
|
+
refresh
|
2143
|
+
render
|
1427
2144
|
end
|
1428
|
-
|
1429
|
-
|
1430
|
-
|
1431
|
-
|
1432
|
-
|
1433
|
-
|
1434
|
-
|
1435
|
-
|
1436
|
-
|
1437
|
-
|
1438
|
-
|
1439
|
-
|
1440
|
-
|
1441
|
-
|
1442
|
-
|
1443
|
-
|
1444
|
-
sed 's/\\;/\\n/g' | sed '/^$/d' | sort -u | sed ':a;N;$!ba;s/\\n/, /g'`.chomp + "/"
|
1445
|
-
system += `grep 'gtk-theme-name' ~/.config/gtk-3.0/* | sed 's/gtk-theme-name=//g' | \\
|
1446
|
-
sed 's/-/ /g'`.sub(/.*:/, '') + "\n"
|
1447
|
-
text += [[251, 0, system]]
|
1448
|
-
cpu = "CPUs = " + `nproc`.chop + " "
|
1449
|
-
cpuinfo = `lscpu`
|
1450
|
-
cpu += cpuinfo[/^.*Model name:\s*(.*)/, 1] + " "
|
1451
|
-
cpu += "Max: " + cpuinfo[/^.*CPU max MHz:\s*(.*)/, 1].to_i.to_s + "MHz "
|
1452
|
-
cpu += "Min: " + cpuinfo[/^.*CPU min MHz:\s*(.*)/, 1].to_i.to_s + "MHz\n\n"
|
1453
|
-
text += [[154, 0, cpu]]
|
1454
|
-
mem = `free -h` + "\n"
|
1455
|
-
text += [[229, 0, mem]]
|
1456
|
-
ps = `ps -eo comm,pid,user,pcpu,pmem,stat --sort -pcpu,-pmem | head` + "\n"
|
1457
|
-
text += [[195, 0, ps]]
|
1458
|
-
disk = `df -H | head -8`
|
1459
|
-
text += [[172, 0, disk]]
|
1460
|
-
dmesg = "\nDMESG (latest first):\n"
|
1461
|
-
dcmd = `dmesg 2>/dev/null | tail -6`.split("\n").sort.reverse.join("\n")
|
1462
|
-
dcmd != "" ? dmesg += dcmd : dmesg += "dmesg requires root, run 'sudo sysctl kernel.dmesg_restrict=0' if you need permission\n"
|
1463
|
-
text += [[219, 0, dmesg]]
|
1464
|
-
w_r_info(ansifix(text))
|
2145
|
+
|
2146
|
+
## Set terminal to raw w/o echo {{{2
|
2147
|
+
$stdin.raw!
|
2148
|
+
$stdin.echo = false
|
2149
|
+
|
2150
|
+
## One-time flush {{{2
|
2151
|
+
$stdin.getc while $stdin.wait_readable(0)
|
2152
|
+
|
2153
|
+
## THE LOOP {{{2
|
2154
|
+
#checkpoint("Program started")
|
2155
|
+
loop do
|
2156
|
+
@dir_old = Dir.pwd
|
2157
|
+
render
|
2158
|
+
getkey
|
2159
|
+
begin # If cwd was deleted externally, jump home
|
2160
|
+
Dir.pwd
|
1465
2161
|
rescue
|
1466
|
-
|
1467
|
-
end
|
1468
|
-
end
|
1469
|
-
# BOTTOM WINDOW FUNCTIONS
|
1470
|
-
def w_b_info(info) # SHOW INFO IN @W_B
|
1471
|
-
@w_b.clr
|
1472
|
-
@w_b.fg, @w_b.bg = 250, @bottomcolor
|
1473
|
-
if info == nil
|
1474
|
-
info = ": for command (use @s for selected item, @t for tagged items) - press ? for help"
|
1475
|
-
info = " Showing only files matching '#{@lsmatch}'" if @lsmatch != ""
|
1476
|
-
info = " Showing only file type '#{@lsfiles}'" if @lsfiles != ""
|
1477
|
-
info = " Showing only file types '#{@lsfiles}'" if @lsfiles =~ /,/
|
1478
|
-
info += " and only files matching '#{@lsmatch}'" if @lsfiles != "" and @lsmatch != ""
|
1479
|
-
info = " Tagged #{@tagged.size} files (#{(@tagsize.to_f/1000000).round(2)}MB)" unless @tagged.empty?
|
1480
|
-
@w_b.fg, @w_b.bg = 250, 88 if @lsfiles != ""
|
1481
|
-
@w_b.fg, @w_b.bg = 250, 21 if @lsmatch != ""
|
1482
|
-
@w_b.fg, @w_b.bg = 250, 55 if @lsfiles != "" and @lsmatch != ""
|
1483
|
-
end
|
1484
|
-
info = info[1..(@w_b.maxx - 3)] + "…" if info.length + 3 > @w_b.maxx
|
1485
|
-
info += " " * (@w_b.maxx - info.length) if info.length < @w_b.maxx
|
1486
|
-
@w_b.text = info
|
1487
|
-
@w_b.write
|
1488
|
-
@w_b.update = false
|
1489
|
-
end
|
1490
|
-
def w_b_getstr(pretext, text, ruby=false) # A SIMPLE READLINE-LIKE ROUTINE
|
1491
|
-
Curses.curs_set(1)
|
1492
|
-
Curses.echo
|
1493
|
-
stk = 0
|
1494
|
-
chr = ""
|
1495
|
-
if ruby
|
1496
|
-
@rubyhistory.insert(stk, text)
|
1497
|
-
@history_copy = @rubyhistory.map(&:clone)
|
1498
|
-
else
|
1499
|
-
@history.insert(stk, text)
|
1500
|
-
@history_copy = @history.map(&:clone)
|
1501
|
-
end
|
1502
|
-
pos = @history_copy[stk].length
|
1503
|
-
while chr != "ENTER"
|
1504
|
-
@w_b.setpos(0,0)
|
1505
|
-
if ruby
|
1506
|
-
init_pair(250, 250, @rubycolor)
|
1507
|
-
else
|
1508
|
-
init_pair(250, 250, @cmdcolor)
|
1509
|
-
end
|
1510
|
-
text = pretext + @history_copy[stk]
|
1511
|
-
text += " " * (@w_b.maxx - text.length) if text.length < @w_b.maxx
|
1512
|
-
@w_b.attron(color_pair(250)) { @w_b << text }
|
1513
|
-
@w_b.setpos(0,pretext.length + pos)
|
1514
|
-
@w_b.refresh
|
1515
|
-
chr = getchr
|
1516
|
-
if chr == 'C-G' or chr == 'C-C'
|
1517
|
-
Curses.curs_set(0)
|
1518
|
-
Curses.noecho
|
1519
|
-
@w_b.update = true
|
1520
|
-
return ""
|
1521
|
-
end
|
1522
|
-
case chr
|
1523
|
-
when 'UP'
|
1524
|
-
unless @w_b.nohistory
|
1525
|
-
unless stk == @history_copy.length - 1
|
1526
|
-
stk += 1
|
1527
|
-
pos = @history_copy[stk].length
|
1528
|
-
end
|
1529
|
-
end
|
1530
|
-
when 'DOWN'
|
1531
|
-
unless @w_b.nohistory
|
1532
|
-
unless stk == 0
|
1533
|
-
stk -= 1
|
1534
|
-
pos = @history_copy[stk].length
|
1535
|
-
end
|
1536
|
-
end
|
1537
|
-
when 'RIGHT'
|
1538
|
-
pos += 1 unless pos > @history_copy[stk].length
|
1539
|
-
when 'LEFT'
|
1540
|
-
pos -= 1 unless pos == 0
|
1541
|
-
when 'HOME'
|
1542
|
-
pos = 0
|
1543
|
-
when 'END'
|
1544
|
-
pos = @history_copy[stk].length
|
1545
|
-
when 'DEL'
|
1546
|
-
@history_copy[stk][pos] = ""
|
1547
|
-
when 'BACK'
|
1548
|
-
unless pos == 0
|
1549
|
-
pos -= 1
|
1550
|
-
@history_copy[stk][pos] = ""
|
1551
|
-
end
|
1552
|
-
when 'WBACK'
|
1553
|
-
unless pos == 0
|
1554
|
-
until @history_copy[stk][pos - 1] == " " or pos == 0
|
1555
|
-
pos -= 1
|
1556
|
-
@history_copy[stk][pos] = ""
|
1557
|
-
end
|
1558
|
-
if @history_copy[stk][pos - 1] == " "
|
1559
|
-
pos -= 1
|
1560
|
-
@history_copy[stk][pos] = ""
|
1561
|
-
end
|
1562
|
-
end
|
1563
|
-
when 'LDEL'
|
1564
|
-
@history_copy[stk] = ""
|
1565
|
-
pos = 0
|
1566
|
-
when 'TAB' # Tab completion of dirs and files
|
1567
|
-
p1 = pos - 1
|
1568
|
-
c = @history_copy[stk][0..(p1)].sub(/^.* /, '')
|
1569
|
-
p0 = p1 - c.length
|
1570
|
-
compl = File.expand_path(c)
|
1571
|
-
compl += "/" if Dir.exist?(compl)
|
1572
|
-
clist = Dir.glob(compl + "*")
|
1573
|
-
unless compl == clist[0].to_s and clist.length == 1
|
1574
|
-
if clist.length == 1
|
1575
|
-
compl = clist[0].to_s
|
1576
|
-
else
|
1577
|
-
ix = clist.find_index(compl)
|
1578
|
-
ix = 0 if ix == nil
|
1579
|
-
sel_item = ""
|
1580
|
-
begin
|
1581
|
-
Curses.curs_set(0)
|
1582
|
-
Curses.noecho
|
1583
|
-
@w_r.clr
|
1584
|
-
@w_r << "Completion list:\n\n"
|
1585
|
-
clist.each.with_index do |item, index|
|
1586
|
-
if index == ix
|
1587
|
-
@w_r.attron(Curses::A_BLINK) { @w_r << item }
|
1588
|
-
sel_item = item
|
1589
|
-
else
|
1590
|
-
@w_r << item
|
1591
|
-
end
|
1592
|
-
@w_r << "\n"
|
1593
|
-
end
|
1594
|
-
@w_r.refresh
|
1595
|
-
ix == clist.length ? ix = 0 : ix += 1
|
1596
|
-
end while getchr == 'TAB'
|
1597
|
-
compl = sel_item
|
1598
|
-
@w_r.clr
|
1599
|
-
Curses.curs_set(1)
|
1600
|
-
Curses.echo
|
1601
|
-
end
|
1602
|
-
end
|
1603
|
-
@history_copy[stk].sub!(c,compl)
|
1604
|
-
pos = pos - c.length + compl.length
|
1605
|
-
when /^.$/
|
1606
|
-
@history_copy[stk].insert(pos,chr)
|
1607
|
-
pos += 1
|
1608
|
-
end
|
1609
|
-
while $stdin.ready?
|
1610
|
-
chr = $stdin.getc
|
1611
|
-
@history_copy[stk].insert(pos,chr)
|
1612
|
-
pos += 1
|
1613
|
-
end
|
1614
|
-
end
|
1615
|
-
curstr = @history_copy[stk]
|
1616
|
-
@history_copy.shift if @w_b.nohistory
|
1617
|
-
ruby ? @rubyhistory.insert(0, @history_copy[stk]) : @history.insert(0, @history_copy[stk])
|
1618
|
-
unless @w_b.nohistory
|
1619
|
-
@history.uniq!
|
1620
|
-
@history.compact!
|
1621
|
-
@history.delete("")
|
1622
|
-
@rubyhistory.uniq!
|
1623
|
-
@rubyhistory.compact!
|
1624
|
-
@rubyhistory.delete("")
|
1625
|
-
end
|
1626
|
-
Curses.curs_set(0)
|
1627
|
-
Curses.noecho
|
1628
|
-
return curstr
|
1629
|
-
end
|
1630
|
-
def w_b_exec(cmd) # EXECUTE COMMAND FROM @W_B
|
1631
|
-
# Subsitute any '@s' with the selected item, @t with tagged items
|
1632
|
-
# 'rm @s' deletes the selected item, 'rm @t' deletes tagged items
|
1633
|
-
return if cmd == ""
|
1634
|
-
@s = "\"#{Dir.pwd}/#{@selected}\""
|
1635
|
-
cmd.gsub!(/@s/, @s)
|
1636
|
-
@t = @tagged.join(" ")
|
1637
|
-
cmd.gsub!(/@t/, @t)
|
1638
|
-
if cmd.match(/^cd /)
|
1639
|
-
cmd.sub!(/^cd (\S*).*/, '\1')
|
1640
|
-
Dir.chdir(cmd) if Dir.exist?(cmd)
|
1641
|
-
return
|
2162
|
+
Dir.chdir
|
1642
2163
|
end
|
1643
|
-
|
1644
|
-
|
1645
|
-
|
1646
|
-
|
1647
|
-
@w_r.pager_cmd = "#{cmd} 2>&1 | #{@bat} -n --color=always"
|
1648
|
-
@w_r.text = %x(#{@w_r.pager_cmd})
|
1649
|
-
rescue
|
1650
|
-
@w_r.pager_cmd = "#{cmd} 2>&1"
|
1651
|
-
@w_r.text = %x(#{@w_r.pager_cmd})
|
1652
|
-
end
|
1653
|
-
unless @w_r.text == "" or @w_r.text == nil
|
1654
|
-
pager_start
|
1655
|
-
pager_show
|
1656
|
-
@w_r.update = false
|
1657
|
-
else
|
1658
|
-
@w_r.update = true
|
1659
|
-
end
|
1660
|
-
}
|
1661
|
-
rescue
|
1662
|
-
w_b_info(" Failed to execute command (#{cmd})")
|
2164
|
+
@index = @directory[Dir.pwd] || 0 if Dir.pwd != @dir_old # If we cd'd, restore index
|
2165
|
+
unless @navi.empty?
|
2166
|
+
command(@navi)
|
2167
|
+
@navi = ''
|
1663
2168
|
end
|
2169
|
+
system("printf \"\033]0;RTFM: #{Dir.pwd}\007\"") # Update window title
|
2170
|
+
rescue StandardError => e
|
2171
|
+
errormsg('⚠ Internal Error', e)
|
1664
2172
|
end
|
1665
2173
|
|
1666
|
-
#
|
1667
|
-
|
1668
|
-
|
1669
|
-
|
1670
|
-
|
1671
|
-
@orderchange = false
|
1672
|
-
begin # Create the four windows/panels
|
1673
|
-
if @border
|
1674
|
-
Curses.stdscr.bg = 236 # Use for borders
|
1675
|
-
Curses.stdscr.fill
|
1676
|
-
else
|
1677
|
-
Curses.stdscr.clear
|
1678
|
-
Curses.stdscr.refresh
|
1679
|
-
end
|
1680
|
-
maxx = Curses.cols
|
1681
|
-
maxy = Curses.lines
|
1682
|
-
# Curses::Window.new(h,w,y,x)
|
1683
|
-
@w_t = Curses::Window.new(1, maxx, 0, 0)
|
1684
|
-
@w_b = Curses::Window.new(1, maxx, maxy - 1, 0)
|
1685
|
-
@w_l = Curses::Window.new(maxy - 3, (maxx * @width / 10) - 1, 2, 0)
|
1686
|
-
@w_r = Curses::Window.new(maxy - 4, maxx - (maxx * @width / 10), 2, maxx * @width / 10)
|
1687
|
-
@w_p = Curses::Window.new(1, maxx - (maxx * @width / 10), maxy - 2, maxx * @width / 10)
|
1688
|
-
@w_p.fg, @w_p.bg = 255, 0
|
1689
|
-
@w_p.refresh
|
1690
|
-
@w_t.fg, @w_t.bg = 232, @topcolor
|
1691
|
-
@w_t.attr = Curses::A_BOLD
|
1692
|
-
@w_b.fg, @w_b.bg = 250, @bottomcolor
|
1693
|
-
@w_b.update = true
|
1694
|
-
@w_r.update = true
|
1695
|
-
@w_r.pager = 0
|
1696
|
-
@w_r.pager_more = false
|
1697
|
-
dir_old = Dir.pwd
|
1698
|
-
lsall_old = @lsall
|
1699
|
-
unless @tagged.empty?
|
1700
|
-
tagged_info
|
1701
|
-
@w_r.update = false
|
1702
|
-
end
|
1703
|
-
loop do # INNER, CORE LOOP
|
1704
|
-
begin # Jump to home dir if current dir is externally removed
|
1705
|
-
Dir.pwd
|
1706
|
-
rescue
|
1707
|
-
Dir.chdir
|
1708
|
-
end
|
1709
|
-
get_files("left")
|
1710
|
-
if Dir.pwd != dir_old
|
1711
|
-
if @directory.key?(Dir.pwd)
|
1712
|
-
@selected = @directory[Dir.pwd]
|
1713
|
-
@index = @files.index(@selected)
|
1714
|
-
else
|
1715
|
-
@index = 0
|
1716
|
-
end
|
1717
|
-
end
|
1718
|
-
dir_old = Dir.pwd
|
1719
|
-
@w_t.bg = @topmatch.find { |name, _| name == "" || dir_old.include?(name) }&.last
|
1720
|
-
@index = 0 if @index == nil
|
1721
|
-
index_old = @index
|
1722
|
-
if @orderchange # Change in ordering must be handled
|
1723
|
-
@index = @files.index(@selected)
|
1724
|
-
@orderchange = false
|
1725
|
-
end
|
1726
|
-
@index = @files.index(@selected) if @lsall != lsall_old # Change in showing all items must be handled
|
1727
|
-
@index = index_old if @files.index(@selected) == nil # If item no longer is shown
|
1728
|
-
@min_index = 0
|
1729
|
-
@max_index = @files.size - 1
|
1730
|
-
@index = @max_index if @index > @max_index # If deleted many items
|
1731
|
-
@index = 0 if @index < 0
|
1732
|
-
@selected = @files[@index] # Get text of selected item
|
1733
|
-
sel_old = @selected_safe
|
1734
|
-
@selected_safe = "\"#{@selected}\"" # Make it safe for commands
|
1735
|
-
system("printf \"\033]0;RTFM: #{Dir.pwd}\007\"") # Set Window title to path
|
1736
|
-
w_t_info # Top window (info line)
|
1737
|
-
@w_l.setpos(0,0)
|
1738
|
-
list_dir(true)
|
1739
|
-
@w_l.refresh
|
1740
|
-
# Bottom window (command line) Before @w_r to avoid image dropping out on startup
|
1741
|
-
w_b_info(nil) if @w_b.update
|
1742
|
-
# Left and right windows (browser & content viewer)
|
1743
|
-
if @w_r.update and @preview
|
1744
|
-
w_r_show
|
1745
|
-
@w_r.fg = 255
|
1746
|
-
end
|
1747
|
-
unless @navi == ""
|
1748
|
-
w_r_info(@navi)
|
1749
|
-
@w_r.update = false
|
1750
|
-
@navi = ""
|
1751
|
-
end
|
1752
|
-
Curses.curs_set(1); Curses.curs_set(0) # Clear residual cursor from editing files
|
1753
|
-
@tag = false # Clear tag pattern
|
1754
|
-
lsall_old = @lsall
|
1755
|
-
main_getkey # Get key from user
|
1756
|
-
break if Curses.cols != maxx or Curses.lines != maxy # break on terminal resize
|
1757
|
-
break if @break # Break to outer loop, redrawing windows, if user hit 'r'
|
1758
|
-
end
|
1759
|
-
rescue StandardError => err # Throw error nicely
|
1760
|
-
w_r_info(err)
|
1761
|
-
@w_r.update = false
|
1762
|
-
ensure # On exit: close curses, clear terminal
|
1763
|
-
@write_conf_all = false
|
1764
|
-
conf_write if @write_conf # Write marks to config file
|
1765
|
-
image_show("clear")
|
1766
|
-
close_screen
|
1767
|
-
# If launched via the script "r", return current dir and "r" will cd to that
|
1768
|
-
begin
|
1769
|
-
File.write(ARGV[0], Dir.pwd) if ARGV[0] and ARGV[0].match(/\/tmp/)
|
1770
|
-
rescue
|
1771
|
-
end
|
1772
|
-
end
|
2174
|
+
at_exit do # Always restore terminal state on quit (or fatal)
|
2175
|
+
$stdin.cooked!
|
2176
|
+
$stdin.echo = true
|
2177
|
+
Rcurses.clear_screen
|
2178
|
+
Cursor.show
|
1773
2179
|
end
|
1774
2180
|
|
1775
|
-
# vim: set sw=2 sts=2 et fdm=
|
2181
|
+
# vim: set sw=2 sts=2 et fdm=marker fdn=2 fcs=fold\:\ :
|