rufio 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +34 -0
- data/README.md +74 -3
- data/README_ja.md +71 -2
- data/Rakefile +14 -1
- data/bin/rufio +31 -2
- data/docs/fast_zip_design_ja.md +167 -0
- data/info/help.md +2 -0
- data/info/keybindings.md +3 -1
- data/info/welcome.md +1 -1
- data/lib/rufio/application.rb +12 -1
- data/lib/rufio/archive_controller.rb +211 -0
- data/lib/rufio/background_command_executor.rb +9 -0
- data/lib/rufio/background_status.rb +49 -0
- data/lib/rufio/bookmark_controller.rb +38 -121
- data/lib/rufio/bookmark_manager.rb +77 -331
- data/lib/rufio/command_completion.rb +6 -1
- data/lib/rufio/command_history.rb +5 -2
- data/lib/rufio/command_mode.rb +40 -134
- data/lib/rufio/command_mode_ui.rb +4 -42
- data/lib/rufio/config.rb +88 -136
- data/lib/rufio/config_loader.rb +3 -11
- data/lib/rufio/dialog_renderer.rb +64 -152
- data/lib/rufio/dialog_support.rb +69 -0
- data/lib/rufio/document_text_extractor.rb +167 -0
- data/lib/rufio/file_operation_controller.rb +49 -222
- data/lib/rufio/file_preview.rb +46 -2
- data/lib/rufio/global_search.rb +121 -9
- data/lib/rufio/help_menu.rb +8 -2
- data/lib/rufio/interpreter_resolver.rb +6 -11
- data/lib/rufio/job_manager.rb +2 -5
- data/lib/rufio/key_input.rb +77 -0
- data/lib/rufio/keybind_handler.rb +81 -115
- data/lib/rufio/local_script_scanner.rb +6 -18
- data/lib/rufio/native/aarch64-linux-gnu/librufio_zig.so +0 -0
- data/lib/rufio/native/aarch64-linux-musl/librufio_zig.so +0 -0
- data/lib/rufio/native/aarch64-macos/librufio_zig.dylib +0 -0
- data/lib/rufio/native/aarch64-windows/rufio_zig.dll +0 -0
- data/lib/rufio/native/x86_64-linux-gnu/librufio_zig.so +0 -0
- data/lib/rufio/native/x86_64-linux-musl/librufio_zig.so +0 -0
- data/lib/rufio/native/x86_64-macos/librufio_zig.dylib +0 -0
- data/lib/rufio/native/x86_64-windows/rufio_zig.dll +0 -0
- data/lib/rufio/native_content_search.rb +153 -0
- data/lib/rufio/native_scanner.rb +13 -68
- data/lib/rufio/native_scanner_zig.rb +106 -56
- data/lib/rufio/native_zip.rb +121 -0
- data/lib/rufio/navigation_controller.rb +18 -20
- data/lib/rufio/screen.rb +2 -12
- data/lib/rufio/script_executor.rb +6 -73
- data/lib/rufio/script_file.rb +44 -0
- data/lib/rufio/script_path_manager.rb +9 -43
- data/lib/rufio/script_runner.rb +13 -50
- data/lib/rufio/search_controller.rb +1 -1
- data/lib/rufio/shell_command_completion.rb +1 -5
- data/lib/rufio/shell_init.rb +90 -0
- data/lib/rufio/task_status.rb +5 -1
- data/lib/rufio/terminal_ui.rb +55 -294
- data/lib/rufio/text_utils.rb +26 -3
- data/lib/rufio/ui_renderer.rb +63 -40
- data/lib/rufio/version.rb +1 -1
- data/lib/rufio/zip_archive.rb +314 -0
- data/lib/rufio/zip_extractor.rb +346 -0
- data/lib/rufio/zip_writer.rb +268 -0
- data/lib/rufio/zoxide_integration.rb +5 -43
- data/lib/rufio.rb +5 -0
- data/lib_zig/rufio_native/Makefile +54 -17
- data/lib_zig/rufio_native/bench.sh +36 -0
- data/lib_zig/rufio_native/build.zig +32 -35
- data/lib_zig/rufio_native/src/cli.zig +99 -0
- data/lib_zig/rufio_native/src/main.zig +187 -83
- data/lib_zig/rufio_native/src/os.zig +889 -0
- data/lib_zig/rufio_native/src/search.zig +919 -0
- data/lib_zig/rufio_native/src/search_test.zig +88 -0
- data/lib_zig/rufio_native/src/zip.zig +482 -0
- metadata +125 -3
- data/lib/rufio/native/rufio_zig.bundle +0 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'dialog_support'
|
|
4
|
+
|
|
3
5
|
require_relative 'file_operations'
|
|
4
6
|
require_relative 'dialog_renderer'
|
|
5
7
|
require_relative 'logger'
|
|
@@ -8,6 +10,8 @@ module Rufio
|
|
|
8
10
|
# ファイル操作専用コントローラ
|
|
9
11
|
# KeybindHandler からファイル作成・削除・リネーム・移動・コピー系メソッドを分離し、単一責任原則に準拠
|
|
10
12
|
class FileOperationController
|
|
13
|
+
include DialogSupport
|
|
14
|
+
|
|
11
15
|
# Dialog size constants
|
|
12
16
|
CONFIRMATION_DIALOG_WIDTH = 45
|
|
13
17
|
DIALOG_BORDER_HEIGHT = 4
|
|
@@ -35,51 +39,15 @@ module Rufio
|
|
|
35
39
|
# ============================
|
|
36
40
|
|
|
37
41
|
def create_file
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
filename = @dialog_renderer.show_input_dialog("Create File", "Enter file name:", {
|
|
41
|
-
border_color: "\e[32m",
|
|
42
|
-
title_color: "\e[1;32m",
|
|
43
|
-
content_color: "\e[37m"
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
return false if filename.nil? || filename.empty?
|
|
47
|
-
|
|
48
|
-
result = @file_operations.create_file(current_path, filename)
|
|
49
|
-
|
|
50
|
-
if result.success
|
|
51
|
-
@directory_listing.refresh
|
|
52
|
-
entries = @directory_listing.list_entries
|
|
53
|
-
new_file_index = entries.find_index { |entry| entry[:name] == filename }
|
|
54
|
-
@nav_controller.select_index(new_file_index) if new_file_index
|
|
42
|
+
prompt_and_select_entry("Create File", "Enter file name:", "\e[32m", "\e[1;32m") do |path, name|
|
|
43
|
+
@file_operations.create_file(path, name)
|
|
55
44
|
end
|
|
56
|
-
|
|
57
|
-
@terminal_ui&.refresh_display
|
|
58
|
-
result.success
|
|
59
45
|
end
|
|
60
46
|
|
|
61
47
|
def create_directory
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
dirname = @dialog_renderer.show_input_dialog("Create Directory", "Enter directory name:", {
|
|
65
|
-
border_color: "\e[34m",
|
|
66
|
-
title_color: "\e[1;34m",
|
|
67
|
-
content_color: "\e[37m"
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
return false if dirname.nil? || dirname.empty?
|
|
71
|
-
|
|
72
|
-
result = @file_operations.create_directory(current_path, dirname)
|
|
73
|
-
|
|
74
|
-
if result.success
|
|
75
|
-
@directory_listing.refresh
|
|
76
|
-
entries = @directory_listing.list_entries
|
|
77
|
-
new_dir_index = entries.find_index { |entry| entry[:name] == dirname }
|
|
78
|
-
@nav_controller.select_index(new_dir_index) if new_dir_index
|
|
48
|
+
prompt_and_select_entry("Create Directory", "Enter directory name:", "\e[34m", "\e[1;34m") do |path, name|
|
|
49
|
+
@file_operations.create_directory(path, name)
|
|
79
50
|
end
|
|
80
|
-
|
|
81
|
-
@terminal_ui&.refresh_display
|
|
82
|
-
result.success
|
|
83
51
|
end
|
|
84
52
|
|
|
85
53
|
# ============================
|
|
@@ -91,27 +59,9 @@ module Rufio
|
|
|
91
59
|
return false unless entry
|
|
92
60
|
|
|
93
61
|
current_name = entry[:name]
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
new_name = @dialog_renderer.show_input_dialog("Rename: #{current_name}", "Enter new name:", {
|
|
97
|
-
border_color: "\e[33m",
|
|
98
|
-
title_color: "\e[1;33m",
|
|
99
|
-
content_color: "\e[37m"
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
return false if new_name.nil? || new_name.empty?
|
|
103
|
-
|
|
104
|
-
result = @file_operations.rename(current_path, current_name, new_name)
|
|
105
|
-
|
|
106
|
-
if result.success
|
|
107
|
-
@directory_listing.refresh
|
|
108
|
-
entries = @directory_listing.list_entries
|
|
109
|
-
new_index = entries.find_index { |e| e[:name] == new_name }
|
|
110
|
-
@nav_controller.select_index(new_index) if new_index
|
|
62
|
+
prompt_and_select_entry("Rename: #{current_name}", "Enter new name:", "\e[33m", "\e[1;33m") do |path, new_name|
|
|
63
|
+
@file_operations.rename(path, current_name, new_name)
|
|
111
64
|
end
|
|
112
|
-
|
|
113
|
-
@terminal_ui&.refresh_display
|
|
114
|
-
result.success
|
|
115
65
|
end
|
|
116
66
|
|
|
117
67
|
# ============================
|
|
@@ -154,31 +104,13 @@ module Rufio
|
|
|
154
104
|
''
|
|
155
105
|
]
|
|
156
106
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
confirmed = false
|
|
162
|
-
show_overlay_dialog(title, content_lines, {
|
|
163
|
-
width: width,
|
|
164
|
-
height: height,
|
|
107
|
+
confirmed = confirm_dialog('Confirm Delete', content_lines, {
|
|
108
|
+
width: [50, current_name.length + 10].max,
|
|
109
|
+
height: content_lines.length + 4,
|
|
165
110
|
border_color: "\e[31m",
|
|
166
111
|
title_color: "\e[1;31m",
|
|
167
112
|
content_color: "\e[37m"
|
|
168
|
-
})
|
|
169
|
-
loop do
|
|
170
|
-
input = STDIN.getch.downcase
|
|
171
|
-
case input
|
|
172
|
-
when 'y'
|
|
173
|
-
confirmed = true
|
|
174
|
-
break
|
|
175
|
-
when 'n', "\e"
|
|
176
|
-
confirmed = false
|
|
177
|
-
break
|
|
178
|
-
end
|
|
179
|
-
end
|
|
180
|
-
nil
|
|
181
|
-
end
|
|
113
|
+
})
|
|
182
114
|
|
|
183
115
|
return false unless confirmed
|
|
184
116
|
|
|
@@ -260,69 +192,39 @@ module Rufio
|
|
|
260
192
|
''
|
|
261
193
|
]
|
|
262
194
|
|
|
263
|
-
|
|
264
|
-
show_overlay_dialog('Exit Confirmation', content_lines, {
|
|
195
|
+
confirm_dialog('Exit Confirmation', content_lines, {
|
|
265
196
|
width: CONFIRMATION_DIALOG_WIDTH,
|
|
266
197
|
height: DIALOG_BORDER_HEIGHT + content_lines.length,
|
|
267
198
|
border_color: "\e[33m",
|
|
268
199
|
title_color: "\e[1;33m",
|
|
269
200
|
content_color: "\e[37m"
|
|
270
|
-
})
|
|
271
|
-
loop do
|
|
272
|
-
input = STDIN.getch.downcase
|
|
273
|
-
case input
|
|
274
|
-
when 'y'
|
|
275
|
-
confirmed = true
|
|
276
|
-
break
|
|
277
|
-
when 'n', "\e", "\x03"
|
|
278
|
-
confirmed = false
|
|
279
|
-
break
|
|
280
|
-
end
|
|
281
|
-
end
|
|
282
|
-
nil
|
|
283
|
-
end
|
|
284
|
-
|
|
285
|
-
confirmed
|
|
201
|
+
}, cancel_on_quit: false)
|
|
286
202
|
end
|
|
287
203
|
|
|
288
204
|
private
|
|
289
205
|
|
|
290
|
-
def
|
|
291
|
-
|
|
292
|
-
@terminal_ui.respond_to?(:screen) &&
|
|
293
|
-
@terminal_ui.respond_to?(:renderer) &&
|
|
294
|
-
@terminal_ui.screen &&
|
|
295
|
-
@terminal_ui.renderer
|
|
296
|
-
|
|
297
|
-
if use_overlay
|
|
298
|
-
@terminal_ui.show_overlay_dialog(title, content_lines, options, &block)
|
|
299
|
-
else
|
|
300
|
-
width = options[:width]
|
|
301
|
-
height = options[:height]
|
|
302
|
-
|
|
303
|
-
unless width && height
|
|
304
|
-
width, height = @dialog_renderer.calculate_dimensions(content_lines, {
|
|
305
|
-
title: title,
|
|
306
|
-
min_width: options[:min_width] || 40,
|
|
307
|
-
max_width: options[:max_width] || 80
|
|
308
|
-
})
|
|
309
|
-
end
|
|
310
|
-
|
|
311
|
-
x, y = @dialog_renderer.calculate_center(width, height)
|
|
206
|
+
def prompt_and_select_entry(title, prompt, border_color, title_color)
|
|
207
|
+
current_path = @directory_listing&.current_path || Dir.pwd
|
|
312
208
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
209
|
+
name = @dialog_renderer.show_input_dialog(title, prompt, {
|
|
210
|
+
border_color: border_color,
|
|
211
|
+
title_color: title_color,
|
|
212
|
+
content_color: "\e[37m"
|
|
213
|
+
})
|
|
318
214
|
|
|
319
|
-
|
|
215
|
+
return false if name.nil? || name.empty?
|
|
320
216
|
|
|
321
|
-
|
|
322
|
-
@terminal_ui&.refresh_display
|
|
217
|
+
result = yield(current_path, name)
|
|
323
218
|
|
|
324
|
-
|
|
219
|
+
if result.success
|
|
220
|
+
@directory_listing.refresh
|
|
221
|
+
entries = @directory_listing.list_entries
|
|
222
|
+
new_index = entries.find_index { |e| e[:name] == name }
|
|
223
|
+
@nav_controller.select_index(new_index) if new_index
|
|
325
224
|
end
|
|
225
|
+
|
|
226
|
+
@terminal_ui&.refresh_display
|
|
227
|
+
result.success
|
|
326
228
|
end
|
|
327
229
|
|
|
328
230
|
def show_delete_confirmation(count, source_path = nil)
|
|
@@ -342,111 +244,37 @@ module Rufio
|
|
|
342
244
|
|
|
343
245
|
print "\a"
|
|
344
246
|
|
|
345
|
-
|
|
346
|
-
show_overlay_dialog(title, content_lines, {
|
|
247
|
+
confirm_dialog(title, content_lines, {
|
|
347
248
|
width: dialog_width,
|
|
348
249
|
height: dialog_height,
|
|
349
250
|
border_color: "\e[31m",
|
|
350
251
|
title_color: "\e[1;31m",
|
|
351
252
|
content_color: "\e[37m"
|
|
352
|
-
})
|
|
353
|
-
loop do
|
|
354
|
-
input = STDIN.getch.downcase
|
|
355
|
-
case input
|
|
356
|
-
when 'y'
|
|
357
|
-
confirmed = true
|
|
358
|
-
break
|
|
359
|
-
when 'n', "\e", "\x03", 'q'
|
|
360
|
-
confirmed = false
|
|
361
|
-
break
|
|
362
|
-
end
|
|
363
|
-
end
|
|
364
|
-
nil
|
|
365
|
-
end
|
|
366
|
-
|
|
367
|
-
confirmed
|
|
253
|
+
})
|
|
368
254
|
end
|
|
369
255
|
|
|
370
256
|
def show_move_confirmation(count, source_path, dest_path)
|
|
371
|
-
|
|
372
|
-
dest_display = shorten_path(dest_path, 35)
|
|
373
|
-
|
|
374
|
-
content_lines = [
|
|
375
|
-
'',
|
|
376
|
-
"Move #{count} item(s)?",
|
|
377
|
-
'',
|
|
378
|
-
"From: #{source_display}",
|
|
379
|
-
"To: #{dest_display}",
|
|
380
|
-
'',
|
|
381
|
-
' [Y]es - Move',
|
|
382
|
-
' [N]o - Cancel',
|
|
383
|
-
''
|
|
384
|
-
]
|
|
385
|
-
|
|
386
|
-
confirmed = false
|
|
387
|
-
show_overlay_dialog('Move Confirmation', content_lines, {
|
|
388
|
-
width: CONFIRMATION_DIALOG_WIDTH,
|
|
389
|
-
height: DIALOG_BORDER_HEIGHT + content_lines.length,
|
|
390
|
-
border_color: "\e[34m",
|
|
391
|
-
title_color: "\e[1;34m",
|
|
392
|
-
content_color: "\e[37m"
|
|
393
|
-
}) do
|
|
394
|
-
loop do
|
|
395
|
-
input = STDIN.getch.downcase
|
|
396
|
-
case input
|
|
397
|
-
when 'y'
|
|
398
|
-
confirmed = true
|
|
399
|
-
break
|
|
400
|
-
when 'n', "\e", "\x03", 'q'
|
|
401
|
-
confirmed = false
|
|
402
|
-
break
|
|
403
|
-
end
|
|
404
|
-
end
|
|
405
|
-
nil
|
|
406
|
-
end
|
|
407
|
-
|
|
408
|
-
confirmed
|
|
257
|
+
show_transfer_confirmation('Move', count, source_path, dest_path, '34')
|
|
409
258
|
end
|
|
410
259
|
|
|
411
260
|
def show_copy_confirmation(count, source_path, dest_path)
|
|
412
|
-
|
|
413
|
-
|
|
261
|
+
show_transfer_confirmation('Copy', count, source_path, dest_path, '32')
|
|
262
|
+
end
|
|
414
263
|
|
|
264
|
+
def show_transfer_confirmation(action, count, source_path, dest_path, color)
|
|
415
265
|
content_lines = [
|
|
416
|
-
'',
|
|
417
|
-
"
|
|
418
|
-
'',
|
|
419
|
-
"
|
|
420
|
-
"To: #{dest_display}",
|
|
421
|
-
'',
|
|
422
|
-
' [Y]es - Copy',
|
|
423
|
-
' [N]o - Cancel',
|
|
424
|
-
''
|
|
266
|
+
'', "#{action} #{count} item(s)?", '',
|
|
267
|
+
"From: #{shorten_path(source_path, 35)}",
|
|
268
|
+
"To: #{shorten_path(dest_path, 35)}", '',
|
|
269
|
+
" [Y]es - #{action}", ' [N]o - Cancel', ''
|
|
425
270
|
]
|
|
426
|
-
|
|
427
|
-
confirmed = false
|
|
428
|
-
show_overlay_dialog('Copy Confirmation', content_lines, {
|
|
271
|
+
confirm_dialog("#{action} Confirmation", content_lines, {
|
|
429
272
|
width: CONFIRMATION_DIALOG_WIDTH,
|
|
430
273
|
height: DIALOG_BORDER_HEIGHT + content_lines.length,
|
|
431
|
-
border_color: "\e[
|
|
432
|
-
title_color: "\e[1
|
|
274
|
+
border_color: "\e[#{color}m",
|
|
275
|
+
title_color: "\e[1;#{color}m",
|
|
433
276
|
content_color: "\e[37m"
|
|
434
|
-
})
|
|
435
|
-
loop do
|
|
436
|
-
input = STDIN.getch.downcase
|
|
437
|
-
case input
|
|
438
|
-
when 'y'
|
|
439
|
-
confirmed = true
|
|
440
|
-
break
|
|
441
|
-
when 'n', "\e", "\x03", 'q'
|
|
442
|
-
confirmed = false
|
|
443
|
-
break
|
|
444
|
-
end
|
|
445
|
-
end
|
|
446
|
-
nil
|
|
447
|
-
end
|
|
448
|
-
|
|
449
|
-
confirmed
|
|
277
|
+
})
|
|
450
278
|
end
|
|
451
279
|
|
|
452
280
|
def show_deletion_result(success_count, total_count, error_messages = [])
|
|
@@ -484,14 +312,13 @@ module Rufio
|
|
|
484
312
|
title_color: title_color,
|
|
485
313
|
content_color: "\e[37m"
|
|
486
314
|
}) do
|
|
487
|
-
|
|
315
|
+
KeyInput.getch
|
|
488
316
|
nil
|
|
489
317
|
end
|
|
490
318
|
end
|
|
491
319
|
|
|
492
320
|
def shorten_path(path, max_length)
|
|
493
|
-
|
|
494
|
-
"...#{path[-(max_length - 3)..-1]}"
|
|
321
|
+
TextUtils.shorten_path_from_end(path, max_length)
|
|
495
322
|
end
|
|
496
323
|
end
|
|
497
324
|
end
|
data/lib/rufio/file_preview.rb
CHANGED
|
@@ -13,6 +13,9 @@ module Rufio
|
|
|
13
13
|
CONTROL_CHAR_NEWLINE = 10
|
|
14
14
|
CONTROL_CHAR_CARRIAGE_RETURN = 13
|
|
15
15
|
|
|
16
|
+
# ZIPプレビューで一覧表示するエントリ数の上限
|
|
17
|
+
ARCHIVE_PREVIEW_ENTRIES = 200
|
|
18
|
+
|
|
16
19
|
def initialize
|
|
17
20
|
# future: hold syntax highlight settings etc.
|
|
18
21
|
end
|
|
@@ -25,6 +28,8 @@ module Rufio
|
|
|
25
28
|
return empty_response if file_size == 0
|
|
26
29
|
|
|
27
30
|
begin
|
|
31
|
+
return archive_response(file_path, max_lines) if zip_archive?(file_path)
|
|
32
|
+
|
|
28
33
|
# binary file detection
|
|
29
34
|
sample = File.binread(file_path, [file_size, BINARY_SAMPLE_SIZE].min)
|
|
30
35
|
return binary_response(file_path) if binary_file?(sample)
|
|
@@ -49,6 +54,45 @@ module Rufio
|
|
|
49
54
|
|
|
50
55
|
private
|
|
51
56
|
|
|
57
|
+
def zip_archive?(file_path)
|
|
58
|
+
File.extname(file_path).casecmp?('.zip') || ZipArchive.zip_signature?(file_path)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# unzip -l 相当のエントリ一覧をプレビュー行として返す
|
|
62
|
+
def archive_response(file_path, max_lines)
|
|
63
|
+
limit = [max_lines, ARCHIVE_PREVIEW_ENTRIES].max
|
|
64
|
+
listing = ZipArchive.list(file_path, limit: limit)
|
|
65
|
+
total_size = listing.entries.sum(&:size)
|
|
66
|
+
|
|
67
|
+
lines = ["ZIP archive: #{listing.total_entries} entries#{' (ZIP64)' if listing.zip64?}", '']
|
|
68
|
+
lines << format('%12s %-8s %-16s %s', 'Length', 'Method', 'Modified', 'Name')
|
|
69
|
+
lines << format('%12s %-8s %-16s %s', '-' * 10, '-' * 8, '-' * 16, '-' * 4)
|
|
70
|
+
listing.entries.each do |entry|
|
|
71
|
+
modified = entry.mtime ? entry.mtime.strftime('%Y-%m-%d %H:%M') : '-'
|
|
72
|
+
method = entry.encrypted? ? "#{ZipArchive.method_name(entry.method)}*" : ZipArchive.method_name(entry.method)
|
|
73
|
+
lines << format('%12d %-8s %-16s %s', entry.size, method, modified, entry.name)
|
|
74
|
+
end
|
|
75
|
+
lines << format('%12s', '-' * 10)
|
|
76
|
+
if listing.truncated?
|
|
77
|
+
lines << "... #{listing.total_entries - listing.entries.length} more entries"
|
|
78
|
+
else
|
|
79
|
+
lines << format('%12d %d files', total_size, listing.entries.length)
|
|
80
|
+
end
|
|
81
|
+
lines << "Comment: #{listing.comment}" unless listing.comment.empty?
|
|
82
|
+
|
|
83
|
+
{
|
|
84
|
+
type: "archive",
|
|
85
|
+
language: nil,
|
|
86
|
+
lines: lines,
|
|
87
|
+
truncated: listing.truncated?,
|
|
88
|
+
size: File.size(file_path),
|
|
89
|
+
modified: File.mtime(file_path),
|
|
90
|
+
encoding: "UTF-8"
|
|
91
|
+
}
|
|
92
|
+
rescue ZipArchive::FormatError => e
|
|
93
|
+
error_response("#{ConfigLoader.message('file.invalid_archive')}: #{e.message}")
|
|
94
|
+
end
|
|
95
|
+
|
|
52
96
|
def binary_file?(sample)
|
|
53
97
|
return false if sample.empty?
|
|
54
98
|
|
|
@@ -77,7 +121,7 @@ module Rufio
|
|
|
77
121
|
end
|
|
78
122
|
|
|
79
123
|
lines << line.chomp
|
|
80
|
-
rescue EncodingError, ArgumentError
|
|
124
|
+
rescue EncodingError, ArgumentError
|
|
81
125
|
# If encoding fails, add placeholder
|
|
82
126
|
lines << "[encoding error in line #{index + 1}]"
|
|
83
127
|
end
|
|
@@ -104,7 +148,7 @@ module Rufio
|
|
|
104
148
|
# Ensure line is properly encoded as UTF-8
|
|
105
149
|
line = line.encode('UTF-8', invalid: :replace, undef: :replace, replace: '?')
|
|
106
150
|
lines << line.chomp
|
|
107
|
-
rescue EncodingError, ArgumentError
|
|
151
|
+
rescue EncodingError, ArgumentError
|
|
108
152
|
# If encoding fails, add placeholder
|
|
109
153
|
lines << "[encoding error in line #{index + 1}]"
|
|
110
154
|
end
|
data/lib/rufio/global_search.rb
CHANGED
|
@@ -3,19 +3,36 @@
|
|
|
3
3
|
require 'thread'
|
|
4
4
|
require_relative 'text_utils'
|
|
5
5
|
require_relative 'config_loader'
|
|
6
|
+
require_relative 'document_text_extractor'
|
|
7
|
+
require_relative 'native_content_search'
|
|
6
8
|
|
|
7
9
|
module Rufio
|
|
8
10
|
# ブックマーク配下のファイル名・テキスト内容をバックグラウンドで横断検索する。
|
|
9
11
|
class GlobalSearch
|
|
10
12
|
SPINNER_FRAMES = ['|', '/', '-', '\\'].freeze
|
|
11
13
|
SPINNER_INTERVAL = 0.12
|
|
14
|
+
# 検索語に一致した部分の表示(黒の太字・黄色背景)
|
|
15
|
+
MATCH_FG = "\e[30m\e[1m"
|
|
16
|
+
MATCH_BG = "\e[43m"
|
|
12
17
|
|
|
13
18
|
attr_reader :query, :results, :errors, :selected_index, :search_type, :skipped_files
|
|
14
19
|
|
|
20
|
+
class << self
|
|
21
|
+
# nil: 利用可能なら Zig 実装を使う(RUFIO_NATIVE_SEARCH=0 で無効化)
|
|
22
|
+
attr_writer :native_search
|
|
23
|
+
|
|
24
|
+
def native_search?
|
|
25
|
+
enabled = @native_search.nil? ? ENV['RUFIO_NATIVE_SEARCH'] != '0' : @native_search
|
|
26
|
+
enabled && NativeContentSearch.available?
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
15
30
|
def initialize
|
|
16
31
|
@active = false
|
|
17
32
|
@scanning = false
|
|
18
33
|
@worker = nil
|
|
34
|
+
@document_extractor = DocumentTextExtractor.new
|
|
35
|
+
@document_cache = DocumentTextCache.new(extractor: @document_extractor)
|
|
19
36
|
end
|
|
20
37
|
|
|
21
38
|
def active?
|
|
@@ -63,8 +80,12 @@ module Rufio
|
|
|
63
80
|
when :entry
|
|
64
81
|
@entries << value
|
|
65
82
|
@results << value if @search_type == :content || matches_query?(value)
|
|
83
|
+
when :entries
|
|
84
|
+
# Zig 実装の結果はまとめて届くので、1 フレーム 256 件の上限に縛られない
|
|
85
|
+
@entries.concat(value)
|
|
86
|
+
@results.concat(value)
|
|
66
87
|
when :error then @errors << value
|
|
67
|
-
when :skipped then @skipped_files += 1
|
|
88
|
+
when :skipped then @skipped_files += value || 1
|
|
68
89
|
when :done then @scanning = false
|
|
69
90
|
end
|
|
70
91
|
end
|
|
@@ -118,11 +139,20 @@ module Rufio
|
|
|
118
139
|
first = [@selected_index - capacity + 1, 0].max
|
|
119
140
|
@results.slice(first, capacity).to_a.each_with_index do |entry, index|
|
|
120
141
|
selected = first + index == @selected_index
|
|
121
|
-
|
|
122
|
-
text
|
|
142
|
+
prefix = "#{selected ? '>' : ' '} [#{entry[:bookmark]}] "
|
|
143
|
+
text = "#{prefix}#{entry[:relative_path]}"
|
|
144
|
+
text += if entry[:location]
|
|
145
|
+
" - #{entry[:location]}"
|
|
146
|
+
elsif entry[:line_number]
|
|
147
|
+
":#{entry[:line_number]}"
|
|
148
|
+
else
|
|
149
|
+
''
|
|
150
|
+
end
|
|
123
151
|
y = index * row_height + 3
|
|
124
|
-
|
|
125
|
-
|
|
152
|
+
# ファイル名検索はパス、本文検索はスニペットの一致箇所をハイライトする
|
|
153
|
+
path_highlight = @search_type == :filename ? prefix.length : nil
|
|
154
|
+
write_line(screen, y, text, width, selected ? "\e[1;36m" : nil, highlight_from: path_highlight)
|
|
155
|
+
write_line(screen, y + 1, " #{entry[:match_text]}", width, highlight_from: 4) if entry[:match_text]
|
|
126
156
|
end
|
|
127
157
|
if @results.empty?
|
|
128
158
|
message = if @bookmark_count.zero?
|
|
@@ -160,9 +190,14 @@ module Rufio
|
|
|
160
190
|
return unless @scanning
|
|
161
191
|
|
|
162
192
|
roots, events, type, query = @roots, @events, @search_type, @query.downcase
|
|
193
|
+
native_cache = self.class.native_search? ? (@native_cache ||= NativeContentSearch::Cache.new) : nil
|
|
163
194
|
@worker = Thread.new do
|
|
164
195
|
begin
|
|
165
|
-
|
|
196
|
+
if native_cache && type == :content && NativeContentSearch.supports_query?(query)
|
|
197
|
+
scan_roots_natively(roots, events, query, native_cache)
|
|
198
|
+
else
|
|
199
|
+
scan_roots(roots, events, type, query)
|
|
200
|
+
end
|
|
166
201
|
rescue StandardError => e
|
|
167
202
|
events << [:error, e.message]
|
|
168
203
|
end
|
|
@@ -230,7 +265,58 @@ module Rufio
|
|
|
230
265
|
end
|
|
231
266
|
end
|
|
232
267
|
|
|
268
|
+
# Zig 実装で走査・検索し、PDF などのドキュメントだけ Ruby 側で抽出する
|
|
269
|
+
def scan_roots_natively(roots, events, query, cache)
|
|
270
|
+
valid_roots = []
|
|
271
|
+
roots.each do |name, root|
|
|
272
|
+
root = File.expand_path(root)
|
|
273
|
+
raise Errno::ENOTDIR, root unless File.directory?(root)
|
|
274
|
+
|
|
275
|
+
valid_roots << [name, root]
|
|
276
|
+
rescue SystemCallError, IOError => e
|
|
277
|
+
events << [:error, "#{name}: #{e.message}"]
|
|
278
|
+
end
|
|
279
|
+
return if valid_roots.empty?
|
|
280
|
+
|
|
281
|
+
engine = NativeContentSearch.new(roots: valid_roots.map(&:last), query: query,
|
|
282
|
+
excluded_extensions: @excluded_extensions,
|
|
283
|
+
document_extensions: DocumentTextExtractor::EXTENSIONS, cache: cache)
|
|
284
|
+
engine.each_batch do |batch|
|
|
285
|
+
entries = []
|
|
286
|
+
batch.each do |kind, *args|
|
|
287
|
+
case kind
|
|
288
|
+
when :match, :document
|
|
289
|
+
name, root = valid_roots[args[0]]
|
|
290
|
+
path = args[1]
|
|
291
|
+
entry = { path: path, bookmark: name, relative_path: path.delete_prefix(root + File::SEPARATOR) }
|
|
292
|
+
if kind == :document
|
|
293
|
+
scan_content(entry, query, events)
|
|
294
|
+
else
|
|
295
|
+
entries << entry.merge(line_number: args[2], match_text: args[3])
|
|
296
|
+
end
|
|
297
|
+
when :error then events << [:error, args[1]]
|
|
298
|
+
when :skipped then events << [:skipped, args[0]]
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
events << [:entries, entries] unless entries.empty?
|
|
302
|
+
end
|
|
303
|
+
ensure
|
|
304
|
+
engine&.close
|
|
305
|
+
end
|
|
306
|
+
|
|
233
307
|
def scan_content(entry, query, events)
|
|
308
|
+
if @document_extractor.supported?(entry[:path])
|
|
309
|
+
@document_cache.each_record(entry[:path]) do |record|
|
|
310
|
+
match_index = record[:search_text].index(query)
|
|
311
|
+
next unless match_index
|
|
312
|
+
|
|
313
|
+
snippet = record[:text][[match_index - 30, 0].max, 200].to_s.strip
|
|
314
|
+
metadata = record.reject { |key, _| key == :text || key == :search_text }
|
|
315
|
+
events << [:entry, entry.merge(metadata).merge(match_text: snippet)]
|
|
316
|
+
end
|
|
317
|
+
return
|
|
318
|
+
end
|
|
319
|
+
|
|
234
320
|
File.open(entry[:path], 'rb') do |file|
|
|
235
321
|
sample = file.read(8192).to_s
|
|
236
322
|
if sample.include?("\0")
|
|
@@ -249,13 +335,39 @@ module Rufio
|
|
|
249
335
|
events << [:entry, entry.merge(line_number: number, match_text: snippet)]
|
|
250
336
|
end
|
|
251
337
|
end
|
|
252
|
-
rescue SystemCallError, IOError => e
|
|
338
|
+
rescue SystemCallError, IOError, DocumentTextExtractor::ExtractionError => e
|
|
253
339
|
events << [:error, "#{entry[:path]}: #{e.message}"]
|
|
254
340
|
end
|
|
255
341
|
|
|
256
|
-
def write_line(screen, y, text, width, color = nil)
|
|
342
|
+
def write_line(screen, y, text, width, color = nil, highlight_from: nil)
|
|
257
343
|
safe_text = text.scrub.gsub(/[[:cntrl:]]/, ' ')
|
|
258
|
-
|
|
344
|
+
visible = TextUtils.truncate_to_width(safe_text, width)
|
|
345
|
+
screen.put_string(0, y, visible, fg: color)
|
|
346
|
+
return unless highlight_from
|
|
347
|
+
|
|
348
|
+
match_ranges(visible, highlight_from).each do |start, length|
|
|
349
|
+
x = TextUtils.display_width(visible[0, start])
|
|
350
|
+
screen.put_string(x, y, visible[start, length], fg: MATCH_FG, bg: MATCH_BG)
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# 検索と同じく大文字小文字を区別せず、from 文字目以降の一致位置を [開始, 長さ] で返す
|
|
355
|
+
def match_ranges(text, from)
|
|
356
|
+
needle = @query.to_s.downcase.chars
|
|
357
|
+
return [] if needle.empty?
|
|
358
|
+
|
|
359
|
+
folded = text.chars.map(&:downcase)
|
|
360
|
+
ranges = []
|
|
361
|
+
index = from
|
|
362
|
+
while index + needle.length <= folded.length
|
|
363
|
+
if folded[index, needle.length] == needle
|
|
364
|
+
ranges << [index, needle.length]
|
|
365
|
+
index += needle.length
|
|
366
|
+
else
|
|
367
|
+
index += 1
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
ranges
|
|
259
371
|
end
|
|
260
372
|
end
|
|
261
373
|
end
|
data/lib/rufio/help_menu.rb
CHANGED
|
@@ -62,6 +62,7 @@ module Rufio
|
|
|
62
62
|
'', 'Selection and file operations',
|
|
63
63
|
"#{key(:select)}: Select / unselect file",
|
|
64
64
|
"#{key(:copy_selected)}/#{key(:move_selected)}/#{key(:delete_selected)}: Copy / move / delete selected files",
|
|
65
|
+
"#{key(:zoxide)}: Extract selected .zip files / compress selected items",
|
|
65
66
|
"#{key(:rename)}/#{key(:delete)}: Rename / delete current file",
|
|
66
67
|
"#{key(:create_file)}/#{key(:create_dir)}: Create file / directory",
|
|
67
68
|
'', 'Search and bookmarks',
|
|
@@ -72,8 +73,9 @@ module Rufio
|
|
|
72
73
|
"#{key(:add_bookmark)}/#{key(:bookmark_menu)}: Add / manage bookmarks",
|
|
73
74
|
'Tab / Shift+Tab: Next / previous bookmark',
|
|
74
75
|
"1-9: Jump to bookmark | #{key(:start_dir)}: Start directory",
|
|
75
|
-
"#{key(:zoxide)}: Directory history (zoxide)",
|
|
76
|
-
"#{key(:quit)}: Quit (with confirmation)"
|
|
76
|
+
"#{key(:zoxide)}: Directory history (zoxide, when nothing is selected)",
|
|
77
|
+
"#{key(:quit)}: Quit (with confirmation)",
|
|
78
|
+
"#{key(:quit_cd)}: Quit and cd to the current directory (rufio --shell-init)"
|
|
77
79
|
]
|
|
78
80
|
when 'Modes'
|
|
79
81
|
[
|
|
@@ -87,6 +89,10 @@ module Rufio
|
|
|
87
89
|
'Search file names or text inside every bookmarked directory.',
|
|
88
90
|
'Tab: Switch File names / Content (literal, case insensitive)',
|
|
89
91
|
'Content results show file, line number and matching text.',
|
|
92
|
+
'Documents: .pdf (page), .xlsx (sheet/cell), .pptx (slide)',
|
|
93
|
+
'Ruby extracts document text; no Python or Office is needed.',
|
|
94
|
+
'Unchanged document text is cached in memory for faster searches.',
|
|
95
|
+
'Images need OCR; legacy .xls/.ppt and slide notes are unsupported.',
|
|
90
96
|
'Yellow Searching... + spinner: Search is running.',
|
|
91
97
|
'Green Search complete: All bookmarked directories scanned.',
|
|
92
98
|
'Exclude extensions in ~/.config/rufio/config.rb:',
|