rufio 1.0.4 → 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 +46 -0
- data/README.md +105 -2
- data/README_ja.md +103 -2
- data/Rakefile +14 -1
- data/bin/rufio +31 -2
- data/docs/fast_zip_design_ja.md +167 -0
- data/examples/config.rb +8 -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 +89 -137
- data/lib/rufio/config_loader.rb +21 -13
- 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 +373 -0
- data/lib/rufio/help_menu.rb +27 -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 +120 -112
- 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/tab_mode_manager.rb +5 -1
- data/lib/rufio/task_status.rb +5 -1
- data/lib/rufio/terminal_ui.rb +116 -293
- data/lib/rufio/text_utils.rb +26 -3
- data/lib/rufio/ui_renderer.rb +68 -41
- 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 +126 -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 'shellwords'
|
|
4
6
|
require_relative 'file_opener'
|
|
5
7
|
require_relative 'filter_manager'
|
|
@@ -11,11 +13,14 @@ require_relative 'selection_manager'
|
|
|
11
13
|
require_relative 'file_operations'
|
|
12
14
|
require_relative 'bookmark_manager'
|
|
13
15
|
require_relative 'zoxide_integration'
|
|
16
|
+
require_relative 'archive_controller'
|
|
14
17
|
require_relative 'dialog_renderer'
|
|
15
18
|
require_relative 'logger'
|
|
16
19
|
|
|
17
20
|
module Rufio
|
|
18
21
|
class KeybindHandler
|
|
22
|
+
include DialogSupport
|
|
23
|
+
|
|
19
24
|
# NavigationController への委譲
|
|
20
25
|
def current_index
|
|
21
26
|
@nav_controller.current_index
|
|
@@ -25,6 +30,55 @@ module Rufio
|
|
|
25
30
|
@nav_controller.preview_focused?
|
|
26
31
|
end
|
|
27
32
|
|
|
33
|
+
def global_search_mode?
|
|
34
|
+
@terminal_ui&.global_search_active? || false
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def start_global_search
|
|
38
|
+
@terminal_ui&.start_global_search(@bookmark_manager.list)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def config_directory
|
|
42
|
+
Config::CONFIG_DIR
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def config_mode?
|
|
46
|
+
return false if @in_log_viewer_mode || @in_help_mode || @job_mode.active?
|
|
47
|
+
|
|
48
|
+
path = @directory_listing&.current_path
|
|
49
|
+
root = File.expand_path(config_directory)
|
|
50
|
+
path && (path == root || path.start_with?(root + File::SEPARATOR))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def enter_config_mode
|
|
54
|
+
return false unless @directory_listing
|
|
55
|
+
|
|
56
|
+
FileUtils.mkdir_p(config_directory)
|
|
57
|
+
leave_other_modes(:config)
|
|
58
|
+
@nav_controller.unfocus_preview_pane
|
|
59
|
+
navigate_to_directory(config_directory)
|
|
60
|
+
rescue SystemCallError => e
|
|
61
|
+
@notification_manager.add("Cannot open config directory: #{e.message}", :error, duration: 0)
|
|
62
|
+
false
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# モード遷移の入口。ヘルプ・検索のオーバーレイは元のモードを保持する。
|
|
66
|
+
def switch_mode(mode)
|
|
67
|
+
case mode
|
|
68
|
+
when :files then leave_other_modes(:files)
|
|
69
|
+
when :logs then enter_log_viewer_mode
|
|
70
|
+
when :jobs then enter_job_mode
|
|
71
|
+
when :config then enter_config_mode
|
|
72
|
+
when :global then start_global_search
|
|
73
|
+
when :help then show_help_dialog
|
|
74
|
+
else false
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def navigate_to_directory(path)
|
|
79
|
+
@nav_controller.navigate_to_directory(path)
|
|
80
|
+
end
|
|
81
|
+
|
|
28
82
|
def preview_scroll_offset
|
|
29
83
|
@nav_controller.preview_scroll_offset
|
|
30
84
|
end
|
|
@@ -78,6 +132,11 @@ module Rufio
|
|
|
78
132
|
@script_path_manager, @notification_manager, @zoxide_integration
|
|
79
133
|
)
|
|
80
134
|
|
|
135
|
+
# ArchiveController(選択アイテムの ZIP 圧縮・展開を担当)
|
|
136
|
+
@archive_controller = ArchiveController.new(
|
|
137
|
+
nil, @dialog_renderer, @selection_manager, @job_manager, @notification_manager
|
|
138
|
+
)
|
|
139
|
+
|
|
81
140
|
# SearchController(fzf・rga検索を担当)
|
|
82
141
|
@search_controller = SearchController.new(nil, @file_opener)
|
|
83
142
|
|
|
@@ -100,12 +159,14 @@ module Rufio
|
|
|
100
159
|
@file_op_controller.set_directory_listing(directory_listing)
|
|
101
160
|
@bookmark_controller.set_directory_listing(directory_listing)
|
|
102
161
|
@search_controller.set_directory_listing(directory_listing)
|
|
162
|
+
@archive_controller.set_directory_listing(directory_listing)
|
|
103
163
|
end
|
|
104
164
|
|
|
105
165
|
def set_terminal_ui(terminal_ui)
|
|
106
166
|
@terminal_ui = terminal_ui
|
|
107
167
|
@nav_controller.set_terminal_ui(terminal_ui)
|
|
108
168
|
@file_op_controller.set_terminal_ui(terminal_ui)
|
|
169
|
+
@archive_controller.set_terminal_ui(terminal_ui)
|
|
109
170
|
@bookmark_controller.set_terminal_ui(terminal_ui)
|
|
110
171
|
# terminal_ui が設定されたら、bookmark_manager と zoxide_integration にも渡す
|
|
111
172
|
@bookmark_manager.set_terminal_ui(terminal_ui)
|
|
@@ -134,6 +195,14 @@ module Rufio
|
|
|
134
195
|
return show_help_dialog
|
|
135
196
|
end
|
|
136
197
|
|
|
198
|
+
if @key_map[key] == :global_search && !@filter_manager.filter_mode
|
|
199
|
+
return start_global_search
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
if @key_map[key] == :config_mode && !@filter_manager.filter_mode
|
|
203
|
+
return enter_config_mode
|
|
204
|
+
end
|
|
205
|
+
|
|
137
206
|
# ジョブモード中の特別処理
|
|
138
207
|
if @job_mode.active?
|
|
139
208
|
return handle_job_mode_key(key)
|
|
@@ -194,6 +263,10 @@ module Rufio
|
|
|
194
263
|
entries[@nav_controller.current_index]
|
|
195
264
|
end
|
|
196
265
|
|
|
266
|
+
def filter_mode?
|
|
267
|
+
@filter_manager.filter_mode
|
|
268
|
+
end
|
|
269
|
+
|
|
197
270
|
def filter_active?
|
|
198
271
|
@filter_manager.filter_active?
|
|
199
272
|
end
|
|
@@ -223,11 +296,6 @@ module Rufio
|
|
|
223
296
|
return false unless @directory_listing
|
|
224
297
|
return true if @in_help_mode
|
|
225
298
|
|
|
226
|
-
exit_log_viewer_mode if @in_log_viewer_mode
|
|
227
|
-
|
|
228
|
-
# 現在のディレクトリを保存
|
|
229
|
-
@pre_help_directory = @directory_listing.current_path
|
|
230
|
-
|
|
231
299
|
# info ディレクトリに移動
|
|
232
300
|
rufio_root = File.expand_path('../..', __dir__)
|
|
233
301
|
info_dir = File.join(rufio_root, 'info')
|
|
@@ -235,6 +303,9 @@ module Rufio
|
|
|
235
303
|
# info ディレクトリが存在することを確認
|
|
236
304
|
return false unless Dir.exist?(info_dir)
|
|
237
305
|
|
|
306
|
+
leave_other_modes(:help_directory)
|
|
307
|
+
@pre_help_directory = @directory_listing.current_path
|
|
308
|
+
|
|
238
309
|
# ヘルプモードを有効化
|
|
239
310
|
@in_help_mode = true
|
|
240
311
|
|
|
@@ -273,14 +344,10 @@ module Rufio
|
|
|
273
344
|
return false unless @directory_listing
|
|
274
345
|
return true if @in_log_viewer_mode
|
|
275
346
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
# 現在のディレクトリを保存
|
|
347
|
+
FileUtils.mkdir_p(@log_dir)
|
|
348
|
+
leave_other_modes(:logs)
|
|
279
349
|
@pre_log_viewer_directory = @directory_listing.current_path
|
|
280
350
|
|
|
281
|
-
# log ディレクトリを作成(存在しない場合)
|
|
282
|
-
FileUtils.mkdir_p(@log_dir) unless Dir.exist?(@log_dir)
|
|
283
|
-
|
|
284
351
|
# log ディレクトリに移動
|
|
285
352
|
navigate_to_directory(@log_dir)
|
|
286
353
|
|
|
@@ -309,45 +376,19 @@ module Rufio
|
|
|
309
376
|
|
|
310
377
|
# ヘルプモード時の制限付き親ディレクトリナビゲーション
|
|
311
378
|
def navigate_parent_with_restriction
|
|
312
|
-
if @in_help_mode
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
379
|
+
restricted_root = if @in_help_mode
|
|
380
|
+
File.join(File.expand_path('../..', __dir__), 'info')
|
|
381
|
+
elsif @in_log_viewer_mode
|
|
382
|
+
@log_dir
|
|
383
|
+
end
|
|
316
384
|
|
|
385
|
+
if restricted_root
|
|
317
386
|
current_path = @directory_listing.current_path
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
unless current_path.start_with?(info_dir)
|
|
321
|
-
return false
|
|
322
|
-
end
|
|
323
|
-
|
|
324
|
-
# 現在のパスが info ディレクトリそのものの場合は移動を許可しない
|
|
325
|
-
if current_path == info_dir
|
|
326
|
-
return false
|
|
327
|
-
end
|
|
328
|
-
|
|
329
|
-
# info ディレクトリ配下であれば、通常のナビゲーションを実行
|
|
330
|
-
navigate_parent
|
|
331
|
-
elsif @in_log_viewer_mode
|
|
332
|
-
# log ディレクトリより上には移動できない
|
|
333
|
-
current_path = @directory_listing.current_path
|
|
334
|
-
|
|
335
|
-
# 現在のパスが log ディレクトリ以下でない場合は移動を許可しない
|
|
336
|
-
unless current_path.start_with?(@log_dir)
|
|
337
|
-
return false
|
|
338
|
-
end
|
|
339
|
-
|
|
340
|
-
# 現在のパスが log ディレクトリそのものの場合は移動を許可しない
|
|
341
|
-
if current_path == @log_dir
|
|
342
|
-
return false
|
|
343
|
-
end
|
|
344
|
-
|
|
345
|
-
# log ディレクトリ配下であれば、通常のナビゲーションを実行
|
|
346
|
-
navigate_parent
|
|
347
|
-
else
|
|
348
|
-
# ヘルプモード・ログビューワモード外では通常のナビゲーション
|
|
349
|
-
navigate_parent
|
|
387
|
+
return false unless current_path.start_with?(restricted_root)
|
|
388
|
+
return false if current_path == restricted_root
|
|
350
389
|
end
|
|
390
|
+
|
|
391
|
+
navigate_parent
|
|
351
392
|
end
|
|
352
393
|
|
|
353
394
|
# プレビューペイン関連メソッド(NavigationController に委譲)
|
|
@@ -402,52 +443,12 @@ module Rufio
|
|
|
402
443
|
|
|
403
444
|
private
|
|
404
445
|
|
|
405
|
-
#
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
# @return [String] 入力されたキー
|
|
412
|
-
def show_overlay_dialog(title, content_lines, options = {}, &block)
|
|
413
|
-
# terminal_ui が利用可能で、screen と renderer が存在する場合のみオーバーレイを使用
|
|
414
|
-
use_overlay = @terminal_ui &&
|
|
415
|
-
@terminal_ui.respond_to?(:screen) &&
|
|
416
|
-
@terminal_ui.respond_to?(:renderer) &&
|
|
417
|
-
@terminal_ui.screen &&
|
|
418
|
-
@terminal_ui.renderer
|
|
419
|
-
|
|
420
|
-
if use_overlay
|
|
421
|
-
# オーバーレイを使用
|
|
422
|
-
@terminal_ui.show_overlay_dialog(title, content_lines, options, &block)
|
|
423
|
-
else
|
|
424
|
-
# フォールバック: 従来の方法
|
|
425
|
-
width = options[:width]
|
|
426
|
-
height = options[:height]
|
|
427
|
-
|
|
428
|
-
unless width && height
|
|
429
|
-
width, height = @dialog_renderer.calculate_dimensions(content_lines, {
|
|
430
|
-
title: title,
|
|
431
|
-
min_width: options[:min_width] || 40,
|
|
432
|
-
max_width: options[:max_width] || 80
|
|
433
|
-
})
|
|
434
|
-
end
|
|
435
|
-
|
|
436
|
-
x, y = @dialog_renderer.calculate_center(width, height)
|
|
437
|
-
|
|
438
|
-
@dialog_renderer.draw_floating_window(x, y, width, height, title, content_lines, {
|
|
439
|
-
border_color: options[:border_color] || "\e[37m",
|
|
440
|
-
title_color: options[:title_color] || "\e[1;33m",
|
|
441
|
-
content_color: options[:content_color] || "\e[37m"
|
|
442
|
-
})
|
|
443
|
-
|
|
444
|
-
key = block_given? ? yield : STDIN.getch
|
|
445
|
-
|
|
446
|
-
@dialog_renderer.clear_area(x, y, width, height)
|
|
447
|
-
@terminal_ui&.refresh_display
|
|
448
|
-
|
|
449
|
-
key
|
|
450
|
-
end
|
|
446
|
+
# 別モードへ入る際の終了順序はここだけで管理する。
|
|
447
|
+
def leave_other_modes(target)
|
|
448
|
+
exit_help_mode if target != :help_directory && help_mode?
|
|
449
|
+
exit_log_viewer_mode if target != :logs && log_viewer_mode?
|
|
450
|
+
exit_job_mode if target != :jobs && in_job_mode?
|
|
451
|
+
true
|
|
451
452
|
end
|
|
452
453
|
|
|
453
454
|
def navigate_parent
|
|
@@ -495,13 +496,14 @@ module Rufio
|
|
|
495
496
|
when :rga_search then rga_search
|
|
496
497
|
when :add_bookmark then add_bookmark
|
|
497
498
|
when :bookmark_menu then show_bookmark_menu
|
|
498
|
-
when :zoxide then
|
|
499
|
+
when :zoxide then archive_or_zoxide
|
|
499
500
|
when :start_dir then goto_start_directory
|
|
500
501
|
when :job_mode then enter_job_mode
|
|
501
502
|
when :help then show_help_dialog
|
|
502
503
|
when :log_viewer then enter_log_viewer_mode
|
|
503
504
|
when :command_mode then activate_command_mode
|
|
504
505
|
when :quit then exit_request
|
|
506
|
+
when :quit_cd then quit_to_current_directory
|
|
505
507
|
else
|
|
506
508
|
false
|
|
507
509
|
end
|
|
@@ -511,6 +513,14 @@ module Rufio
|
|
|
511
513
|
@file_op_controller.show_exit_confirmation
|
|
512
514
|
end
|
|
513
515
|
|
|
516
|
+
# 確認なしで終了し、シェル関数(rufio --shell-init)が移動する先として現在のディレクトリを伝える。
|
|
517
|
+
# ヘルプ・ログビューワは rufio 内部のディレクトリを表示しているため対象外。
|
|
518
|
+
def quit_to_current_directory
|
|
519
|
+
return false if @in_help_mode || @in_log_viewer_mode
|
|
520
|
+
|
|
521
|
+
:quit_cd
|
|
522
|
+
end
|
|
523
|
+
|
|
514
524
|
def fzf_search
|
|
515
525
|
@search_controller.fzf_search
|
|
516
526
|
end
|
|
@@ -548,8 +558,6 @@ module Rufio
|
|
|
548
558
|
true
|
|
549
559
|
end
|
|
550
560
|
|
|
551
|
-
|
|
552
|
-
|
|
553
561
|
def goto_start_directory
|
|
554
562
|
@bookmark_controller.goto_start_directory
|
|
555
563
|
end
|
|
@@ -573,17 +581,11 @@ module Rufio
|
|
|
573
581
|
@bookmark_controller.show_script_paths_manager
|
|
574
582
|
end
|
|
575
583
|
|
|
576
|
-
|
|
577
|
-
|
|
584
|
+
# 選択中のアイテムがあれば ZIP 圧縮・展開、なければ zoxide
|
|
585
|
+
def archive_or_zoxide
|
|
586
|
+
return @archive_controller.archive_selected if @selection_manager.any? && !@in_help_mode && !@in_log_viewer_mode
|
|
578
587
|
|
|
579
|
-
|
|
580
|
-
if result
|
|
581
|
-
@nav_controller.select_index(0)
|
|
582
|
-
@nav_controller.clear_filter_mode
|
|
583
|
-
true
|
|
584
|
-
else
|
|
585
|
-
false
|
|
586
|
-
end
|
|
588
|
+
show_zoxide_menu
|
|
587
589
|
end
|
|
588
590
|
|
|
589
591
|
# zoxide 機能
|
|
@@ -599,11 +601,9 @@ module Rufio
|
|
|
599
601
|
|
|
600
602
|
# カーソルを画面下部の入力行に移動
|
|
601
603
|
def move_to_input_line
|
|
602
|
-
|
|
603
|
-
#
|
|
604
|
-
|
|
605
|
-
print "\e[#{screen_height};1H" # 最終行の先頭にカーソル移動
|
|
606
|
-
print "\e[2K" # 行全体をクリア
|
|
604
|
+
screen_height = @terminal_ui&.screen_height || 24
|
|
605
|
+
print "\e[#{screen_height};1H"
|
|
606
|
+
print "\e[2K"
|
|
607
607
|
end
|
|
608
608
|
|
|
609
609
|
# Escキーでキャンセル可能な入力処理
|
|
@@ -613,7 +613,7 @@ module Rufio
|
|
|
613
613
|
input = []
|
|
614
614
|
|
|
615
615
|
loop do
|
|
616
|
-
char =
|
|
616
|
+
char = KeyInput.getch
|
|
617
617
|
|
|
618
618
|
case char
|
|
619
619
|
when "\e" # Escape
|
|
@@ -657,6 +657,9 @@ module Rufio
|
|
|
657
657
|
|
|
658
658
|
# ジョブモードに入る
|
|
659
659
|
def enter_job_mode
|
|
660
|
+
return true if @job_mode.active?
|
|
661
|
+
|
|
662
|
+
leave_other_modes(:jobs)
|
|
660
663
|
@job_mode.activate
|
|
661
664
|
@terminal_ui&.set_job_mode(@job_mode, @job_manager, @notification_manager) if @terminal_ui
|
|
662
665
|
true
|
|
@@ -695,6 +698,11 @@ module Rufio
|
|
|
695
698
|
end
|
|
696
699
|
|
|
697
700
|
# ジョブがあるかどうか
|
|
701
|
+
# バックグラウンドの圧縮・展開ジョブの進捗・完了を反映する(メインループから呼ぶ)
|
|
702
|
+
def poll_archive_jobs
|
|
703
|
+
@archive_controller.poll
|
|
704
|
+
end
|
|
705
|
+
|
|
698
706
|
def has_jobs?
|
|
699
707
|
@job_manager.has_jobs?
|
|
700
708
|
end
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'script_file'
|
|
4
|
+
|
|
3
5
|
module Rufio
|
|
4
6
|
# 閲覧中ディレクトリのスクリプトファイルを検出するスキャナー
|
|
5
7
|
# カレントディレクトリ直下のスクリプトファイル(.sh, .rb, .py等)を検出し、
|
|
6
8
|
# コマンドモードから @script.sh 形式で実行可能にする
|
|
7
9
|
class LocalScriptScanner
|
|
8
10
|
# サポートするスクリプト拡張子
|
|
9
|
-
SUPPORTED_EXTENSIONS =
|
|
11
|
+
SUPPORTED_EXTENSIONS = ScriptFile::EXTENSIONS
|
|
10
12
|
|
|
11
13
|
# @param directory [String, nil] スキャン対象ディレクトリ
|
|
12
14
|
def initialize(directory = nil)
|
|
@@ -33,27 +35,14 @@ module Rufio
|
|
|
33
35
|
# @param name [String] スクリプト名(拡張子あり/なし)
|
|
34
36
|
# @return [Hash, nil] スクリプト情報 { name:, path:, dir: } または nil
|
|
35
37
|
def find_script(name)
|
|
36
|
-
|
|
37
|
-
script = available_scripts.find { |s| s[:name] == name }
|
|
38
|
-
return script if script
|
|
39
|
-
|
|
40
|
-
# 拡張子なしで検索
|
|
41
|
-
SUPPORTED_EXTENSIONS.each do |ext|
|
|
42
|
-
script = available_scripts.find { |s| s[:name] == "#{name}#{ext}" }
|
|
43
|
-
return script if script
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
nil
|
|
38
|
+
ScriptFile.find(available_scripts, name)
|
|
47
39
|
end
|
|
48
40
|
|
|
49
41
|
# Tab補完候補を取得
|
|
50
42
|
# @param prefix [String] 入力中の文字列
|
|
51
43
|
# @return [Array<String>] 補完候補のスクリプト名
|
|
52
44
|
def complete(prefix)
|
|
53
|
-
available_scripts
|
|
54
|
-
.map { |s| s[:name] }
|
|
55
|
-
.select { |name| name.start_with?(prefix) }
|
|
56
|
-
.sort
|
|
45
|
+
ScriptFile.complete(available_scripts, prefix)
|
|
57
46
|
end
|
|
58
47
|
|
|
59
48
|
private
|
|
@@ -85,8 +74,7 @@ module Rufio
|
|
|
85
74
|
# @param filename [String] ファイル名
|
|
86
75
|
# @return [Boolean]
|
|
87
76
|
def script_file?(filename)
|
|
88
|
-
|
|
89
|
-
SUPPORTED_EXTENSIONS.include?(ext)
|
|
77
|
+
ScriptFile.supported?(filename)
|
|
90
78
|
end
|
|
91
79
|
end
|
|
92
80
|
end
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fiddle'
|
|
4
|
+
require_relative 'native_scanner'
|
|
5
|
+
require_relative 'native_scanner_zig'
|
|
6
|
+
|
|
7
|
+
module Rufio
|
|
8
|
+
# Zig 実装のコンテンツ検索(G mode 用)。
|
|
9
|
+
# Ruby 実装(GlobalSearch#scan_content)と同じ結果を、並列走査と SIMD 検索で返す。
|
|
10
|
+
class NativeContentSearch
|
|
11
|
+
HEADER_SIZE = 25
|
|
12
|
+
HEADER_FORMAT = 'CL<Q<L<L<L<'
|
|
13
|
+
STATE_SCANNING = 1
|
|
14
|
+
KINDS = { 1 => :match, 2 => :document, 3 => :error }.freeze
|
|
15
|
+
MAX_QUERY_BYTES = 1024
|
|
16
|
+
POLL_INTERVAL = 0.001
|
|
17
|
+
|
|
18
|
+
# 検索をまたいでファイル内容を保持するキャッシュ(mtime/inode/サイズで毎回検証する)
|
|
19
|
+
class Cache
|
|
20
|
+
DEFAULT_MAX_BYTES = 256 * 1024 * 1024
|
|
21
|
+
|
|
22
|
+
attr_reader :handle
|
|
23
|
+
|
|
24
|
+
def self.finalizer(handle)
|
|
25
|
+
proc { NativeScannerZigFFI.core_cache_destroy(handle) }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def initialize(max_bytes: DEFAULT_MAX_BYTES)
|
|
29
|
+
@handle = NativeScannerZigFFI.core_cache_create(max_bytes)
|
|
30
|
+
raise StandardError, 'Failed to create search cache' if @handle.zero?
|
|
31
|
+
|
|
32
|
+
ObjectSpace.define_finalizer(self, self.class.finalizer(@handle))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def cached_bytes
|
|
36
|
+
@handle.zero? ? 0 : NativeScannerZigFFI.core_cache_bytes(@handle)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# このキャッシュを使う検索をすべて close してから呼ぶこと
|
|
40
|
+
def close
|
|
41
|
+
return if @handle.zero?
|
|
42
|
+
|
|
43
|
+
ObjectSpace.undefine_finalizer(self)
|
|
44
|
+
NativeScannerZigFFI.core_cache_destroy(@handle)
|
|
45
|
+
@handle = 0
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.available?
|
|
50
|
+
NativeScannerZigFFI.search_available?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Zig 側は ASCII のみ大文字小文字を畳み込むため、大文字小文字を持つ非 ASCII 文字は Ruby 実装に任せる
|
|
54
|
+
def self.supports_query?(query)
|
|
55
|
+
return false if query.empty? || query.bytesize > MAX_QUERY_BYTES
|
|
56
|
+
|
|
57
|
+
query.each_char.all? { |char| char.ascii_only? || (char.upcase == char && char.downcase == char) }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# マッチ周辺のバイト列から Ruby 実装と同じスニペット(30 文字前から 200 文字)を作る
|
|
61
|
+
def self.snippet(window, offset)
|
|
62
|
+
line = window.dup.force_encoding(Encoding::UTF_8).scrub
|
|
63
|
+
match_index = window.byteslice(0, offset).force_encoding(Encoding::UTF_8).scrub.length
|
|
64
|
+
line[[match_index - 30, 0].max, 200].to_s.strip
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def initialize(roots:, query:, excluded_extensions: [], document_extensions: [], cache: nil, threads: 0)
|
|
68
|
+
raise StandardError, 'Zig content search is not available' unless self.class.available?
|
|
69
|
+
|
|
70
|
+
@cache = cache
|
|
71
|
+
@path_encoding = Encoding.find('filesystem')
|
|
72
|
+
roots_buf = pack(roots)
|
|
73
|
+
query_buf = query.b
|
|
74
|
+
excluded_buf = pack(excluded_extensions)
|
|
75
|
+
documents_buf = pack(document_extensions)
|
|
76
|
+
@handle = NativeScannerZigFFI.core_search_create(
|
|
77
|
+
roots_buf, roots_buf.bytesize, query_buf, query_buf.bytesize,
|
|
78
|
+
excluded_buf, excluded_buf.bytesize, documents_buf, documents_buf.bytesize,
|
|
79
|
+
threads, cache ? cache.handle : 0
|
|
80
|
+
)
|
|
81
|
+
raise StandardError, 'Failed to start native search' if @handle.zero?
|
|
82
|
+
|
|
83
|
+
@length = Fiddle::Pointer.malloc(8, Fiddle::RUBY_FREE)
|
|
84
|
+
@skipped = 0
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# [:match, root_index, path, line_number, snippet] / [:document, root_index, path] /
|
|
88
|
+
# [:error, root_index, message] / [:skipped, count] を検索終了まで順に返す
|
|
89
|
+
def each_event(&block)
|
|
90
|
+
return enum_for(:each_event) unless block_given?
|
|
91
|
+
|
|
92
|
+
each_batch { |batch| batch.each(&block) }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# 1 回の取り出しごとにイベント配列を返す(UI へまとめて渡すため)
|
|
96
|
+
def each_batch
|
|
97
|
+
return enum_for(:each_batch) unless block_given?
|
|
98
|
+
|
|
99
|
+
loop do
|
|
100
|
+
state = NativeScannerZigFFI.core_search_state(@handle)
|
|
101
|
+
data = drain
|
|
102
|
+
batch = []
|
|
103
|
+
each_record(data) { |event| batch << event }
|
|
104
|
+
skipped = NativeScannerZigFFI.core_search_skipped(@handle)
|
|
105
|
+
if skipped > @skipped
|
|
106
|
+
batch << [:skipped, skipped - @skipped]
|
|
107
|
+
@skipped = skipped
|
|
108
|
+
end
|
|
109
|
+
yield batch unless batch.empty?
|
|
110
|
+
break if data.empty? && state != STATE_SCANNING
|
|
111
|
+
|
|
112
|
+
sleep POLL_INTERVAL if data.empty?
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def close
|
|
117
|
+
return if @handle.nil? || @handle.zero?
|
|
118
|
+
|
|
119
|
+
NativeScannerZigFFI.core_search_destroy(@handle)
|
|
120
|
+
@handle = 0
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
def pack(values)
|
|
126
|
+
values.map { |value| value.to_s.b }.join("\0")
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def drain
|
|
130
|
+
pointer = NativeScannerZigFFI.core_search_drain(@handle, @length)
|
|
131
|
+
length = @length[0, 8].unpack1('Q')
|
|
132
|
+
length.zero? ? ''.b : pointer.to_str(length)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def each_record(data)
|
|
136
|
+
position = 0
|
|
137
|
+
while position + HEADER_SIZE <= data.bytesize
|
|
138
|
+
kind, root, line, offset, path_length, extra_length = data.byteslice(position, HEADER_SIZE).unpack(HEADER_FORMAT)
|
|
139
|
+
position += HEADER_SIZE
|
|
140
|
+
path = data.byteslice(position, path_length).force_encoding(@path_encoding)
|
|
141
|
+
position += path_length
|
|
142
|
+
extra = data.byteslice(position, extra_length)
|
|
143
|
+
position += extra_length
|
|
144
|
+
|
|
145
|
+
case KINDS[kind]
|
|
146
|
+
when :match then yield [:match, root, path, line, self.class.snippet(extra, offset)]
|
|
147
|
+
when :document then yield [:document, root, path]
|
|
148
|
+
when :error then yield [:error, root, "#{path}: #{extra.force_encoding(Encoding::UTF_8).scrub}"]
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|