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
data/lib/rufio/terminal_ui.rb
CHANGED
|
@@ -3,39 +3,19 @@
|
|
|
3
3
|
require 'io/console'
|
|
4
4
|
require_relative 'text_utils'
|
|
5
5
|
require_relative 'help_menu'
|
|
6
|
+
require_relative 'global_search'
|
|
7
|
+
require_relative 'ui_renderer'
|
|
6
8
|
|
|
7
9
|
module Rufio
|
|
8
10
|
class TerminalUI
|
|
9
|
-
# Layout constants
|
|
10
|
-
HEADER_HEIGHT = 1 # Header占有行数(モードタブ+パス+バージョン 1行に統合)
|
|
11
|
-
FOOTER_HEIGHT = 1 # Footer占有行数(ブックマーク一覧 + ステータス情報)
|
|
12
|
-
HEADER_FOOTER_MARGIN = 2 # Header(1行) + Footer(1行)分のマージン
|
|
13
|
-
|
|
14
|
-
# Panel layout ratios
|
|
15
|
-
LEFT_PANEL_RATIO = 0.5 # 左パネルの幅比率
|
|
16
|
-
RIGHT_PANEL_RATIO = 1.0 - LEFT_PANEL_RATIO
|
|
17
|
-
|
|
18
11
|
# Display constants
|
|
19
12
|
DEFAULT_SCREEN_WIDTH = 80 # デフォルト画面幅
|
|
20
13
|
DEFAULT_SCREEN_HEIGHT = 24 # デフォルト画面高さ
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
TAB_SEPARATOR = ">" # タブ間セパレータ
|
|
24
|
-
|
|
25
|
-
# File display constants
|
|
26
|
-
ICON_SIZE_PADDING = 12 # アイコン、選択マーク、サイズ情報分
|
|
27
|
-
CURSOR_OFFSET = 1 # カーソル位置のオフセット
|
|
28
|
-
|
|
29
|
-
# Size display constants (bytes)
|
|
30
|
-
KILOBYTE = 1024
|
|
31
|
-
MEGABYTE = KILOBYTE * 1024
|
|
32
|
-
GIGABYTE = MEGABYTE * 1024
|
|
14
|
+
HEADER_FOOTER_MARGIN = UIRenderer::HEADER_FOOTER_MARGIN
|
|
15
|
+
CONTENT_START_LINE = UIRenderer::CONTENT_START_LINE
|
|
33
16
|
|
|
34
|
-
#
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
# Line offsets
|
|
38
|
-
CONTENT_START_LINE = 1 # コンテンツ開始行(フッタ1行: Y=0)
|
|
17
|
+
# T キーで終了した時のディレクトリ(それ以外の終了では nil)
|
|
18
|
+
attr_reader :exit_directory
|
|
39
19
|
|
|
40
20
|
def initialize(test_mode: false)
|
|
41
21
|
console = IO.console
|
|
@@ -53,6 +33,7 @@ module Rufio
|
|
|
53
33
|
@input_thread = nil
|
|
54
34
|
@command_mode_active = false
|
|
55
35
|
@help_menu = HelpMenu.new
|
|
36
|
+
@global_search = GlobalSearch.new
|
|
56
37
|
@command_input = ""
|
|
57
38
|
@command_mode = CommandMode.new
|
|
58
39
|
@dialog_renderer = DialogRenderer.new
|
|
@@ -71,17 +52,6 @@ module Rufio
|
|
|
71
52
|
@in_job_mode = false
|
|
72
53
|
@job_mode_needs_redraw = false
|
|
73
54
|
|
|
74
|
-
# Preview cache
|
|
75
|
-
@preview_cache = {}
|
|
76
|
-
@last_preview_path = nil
|
|
77
|
-
|
|
78
|
-
# シンタックスハイライター(bat が利用可能な場合のみ動作)
|
|
79
|
-
@syntax_highlighter = SyntaxHighlighter.new
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
# Tab mode manager
|
|
83
|
-
@tab_mode_manager = TabModeManager.new
|
|
84
|
-
|
|
85
55
|
# UIRenderer(描画ロジックを担当)
|
|
86
56
|
ui_opts = ConfigLoader.ui_options
|
|
87
57
|
@ui_renderer = UIRenderer.new(
|
|
@@ -91,9 +61,11 @@ module Rufio
|
|
|
91
61
|
left_panel_ratio: ui_opts[:panel_ratio],
|
|
92
62
|
preview_enabled: ui_opts[:preview_enabled]
|
|
93
63
|
)
|
|
64
|
+
@tab_mode_manager = @ui_renderer.tab_mode_manager
|
|
65
|
+
@background_status = @ui_renderer.background_status
|
|
94
66
|
end
|
|
95
67
|
|
|
96
|
-
attr_reader :ui_renderer
|
|
68
|
+
attr_reader :ui_renderer, :screen_width, :screen_height
|
|
97
69
|
|
|
98
70
|
def start(directory_listing, keybind_handler, file_preview, background_executor = nil)
|
|
99
71
|
@directory_listing = directory_listing
|
|
@@ -135,24 +107,10 @@ module Rufio
|
|
|
135
107
|
# ウィンドウサイズを更新してから画面をクリアして再描画
|
|
136
108
|
update_screen_size
|
|
137
109
|
|
|
138
|
-
|
|
139
|
-
@
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
# ブックマークキャッシュもクリア
|
|
143
|
-
@cached_bookmarks = nil
|
|
144
|
-
@cached_bookmark_time = nil
|
|
145
|
-
|
|
146
|
-
# バッファベースの描画が利用可能な場合は全画面を再描画
|
|
147
|
-
if @screen && @renderer
|
|
148
|
-
# レンダラーの前フレーム情報をリセット(差分レンダリングを強制的に全体描画にする)
|
|
149
|
-
@renderer.clear
|
|
150
|
-
@screen.clear
|
|
151
|
-
draw_screen_to_buffer(@screen, nil, nil)
|
|
152
|
-
@renderer.render(@screen)
|
|
153
|
-
# カーソルを画面外に移動
|
|
154
|
-
print "\e[#{@screen_height};#{@screen_width}H"
|
|
155
|
-
end
|
|
110
|
+
@ui_renderer.clear_preview_cache
|
|
111
|
+
@ui_renderer.clear_bookmark_cache
|
|
112
|
+
|
|
113
|
+
render_full_screen
|
|
156
114
|
end
|
|
157
115
|
|
|
158
116
|
# スクリプトランナーを設定
|
|
@@ -205,6 +163,8 @@ module Rufio
|
|
|
205
163
|
rescue
|
|
206
164
|
# スレッド終了
|
|
207
165
|
end
|
|
166
|
+
# ダイアログのキー入力もこのキューから読ませ、入力スレッドとの奪い合いを防ぐ
|
|
167
|
+
KeyInput.attach(@input_queue)
|
|
208
168
|
end
|
|
209
169
|
|
|
210
170
|
# SGR拡張マウスレポートを有効化
|
|
@@ -236,11 +196,14 @@ module Rufio
|
|
|
236
196
|
end
|
|
237
197
|
|
|
238
198
|
# エスケープシーケンスの後続バイトを読み取る(矢印キー等の短いシーケンス用)。
|
|
239
|
-
# Windows: IO.select
|
|
199
|
+
# Windows: IO.select を使わずキューから取得。
|
|
240
200
|
def read_next_input_byte
|
|
241
201
|
if windows?
|
|
242
|
-
# ConPTY ではバイト到着が遅れることがあるため 5ms では ESC
|
|
243
|
-
|
|
202
|
+
# ConPTY ではバイト到着が遅れることがあるため 5ms では ESC 単体と誤判定する。
|
|
203
|
+
# G mode は検索中に全画面を再描画し続けて遅延が大きくなりやすく、
|
|
204
|
+
# しかも ESC 単体で閉じてしまうため、待ち時間を長めに取る。
|
|
205
|
+
timeout = global_search_active? ? 0.1 : 0.025
|
|
206
|
+
b = windows_queue_pop_with_timeout(timeout)
|
|
244
207
|
b.nil? ? nil : b.chr(Encoding::BINARY)
|
|
245
208
|
else
|
|
246
209
|
STDIN.read_nonblock(1) rescue nil
|
|
@@ -261,49 +224,16 @@ module Rufio
|
|
|
261
224
|
# Windows 専用: キューから1文字(マルチバイト対応)を組み立てる。
|
|
262
225
|
# first_byte_int は getbyte が返す Integer。
|
|
263
226
|
def windows_build_char(first_byte_int)
|
|
264
|
-
|
|
265
|
-
when 0x00..0x7F then 0
|
|
266
|
-
when 0xC0..0xDF then 1
|
|
267
|
-
when 0xE0..0xEF then 2
|
|
268
|
-
when 0xF0..0xF7 then 3
|
|
269
|
-
else 0
|
|
270
|
-
end
|
|
271
|
-
if remaining == 0
|
|
272
|
-
first_byte_int.chr(Encoding::UTF_8)
|
|
273
|
-
else
|
|
274
|
-
buf = first_byte_int.chr(Encoding::BINARY)
|
|
275
|
-
remaining.times do
|
|
276
|
-
b = windows_queue_pop_with_timeout(0.005)
|
|
277
|
-
return nil if b.nil?
|
|
278
|
-
buf << b.chr(Encoding::BINARY)
|
|
279
|
-
end
|
|
280
|
-
result = buf.force_encoding(Encoding::UTF_8)
|
|
281
|
-
result.valid_encoding? ? result : nil
|
|
282
|
-
end
|
|
227
|
+
KeyInput.build_char(first_byte_int, @input_queue)
|
|
283
228
|
end
|
|
284
229
|
|
|
285
230
|
# Windows 専用: @input_queue からタイムアウト付きで Integer バイトを取り出す。
|
|
286
|
-
# Ruby 3.2+ は Queue#pop(timeout:) で正確に待機する(sleep ポーリングは
|
|
287
|
-
# Windows のタイマー分解能 約15ms により遅延する)。それ以前はポーリング。
|
|
288
|
-
QUEUE_POP_TIMEOUT_SUPPORTED = Queue.instance_method(:pop).parameters.any? { |_, n| n == :timeout }
|
|
289
|
-
|
|
290
231
|
def windows_queue_pop_with_timeout(timeout_sec)
|
|
291
|
-
|
|
292
|
-
return @input_queue.pop(timeout: timeout_sec)
|
|
293
|
-
end
|
|
294
|
-
|
|
295
|
-
deadline = Time.now + timeout_sec
|
|
296
|
-
loop do
|
|
297
|
-
begin
|
|
298
|
-
return @input_queue.pop(true)
|
|
299
|
-
rescue ThreadError
|
|
300
|
-
return nil if Time.now >= deadline
|
|
301
|
-
sleep 0.001
|
|
302
|
-
end
|
|
303
|
-
end
|
|
232
|
+
KeyInput.pop_with_timeout(@input_queue, timeout_sec)
|
|
304
233
|
end
|
|
305
234
|
|
|
306
235
|
def cleanup_terminal
|
|
236
|
+
@global_search.close
|
|
307
237
|
# マウスレポートを先に無効化して新しいイベントの流入を止める
|
|
308
238
|
if windows?
|
|
309
239
|
print "\e[?1000l\e[?1006l"
|
|
@@ -314,6 +244,7 @@ module Rufio
|
|
|
314
244
|
|
|
315
245
|
# Windows 入力スレッドを停止
|
|
316
246
|
if windows? && @input_thread
|
|
247
|
+
KeyInput.detach
|
|
317
248
|
@stop_input_thread = true
|
|
318
249
|
@input_thread.kill rescue nil
|
|
319
250
|
@input_thread.join(0.5) rescue nil
|
|
@@ -362,7 +293,6 @@ module Rufio
|
|
|
362
293
|
# 最小スリープ時間(入力チェック間隔)
|
|
363
294
|
# Windows は入力をキュー経由で受けるため、入力遅延を抑えるよう短めにする
|
|
364
295
|
min_sleep_interval = windows? ? 0.008 : 0.0333 # Windows: 約8ms / Unix: 30FPS
|
|
365
|
-
check_interval = 0.1 # バックグラウンドタスクのチェック間隔
|
|
366
296
|
|
|
367
297
|
# Phase 3: Screen/Rendererを初期化
|
|
368
298
|
@screen = Screen.new(@screen_width, @screen_height)
|
|
@@ -373,13 +303,6 @@ module Rufio
|
|
|
373
303
|
draw_screen_to_buffer(@screen, nil, nil)
|
|
374
304
|
@renderer.render(@screen)
|
|
375
305
|
|
|
376
|
-
last_notification_check = Time.now
|
|
377
|
-
last_lamp_check = Time.now
|
|
378
|
-
notification_message = nil
|
|
379
|
-
notification_time = nil
|
|
380
|
-
previous_notification = nil
|
|
381
|
-
previous_lamp_message = @ui_renderer.completion_lamp_message
|
|
382
|
-
|
|
383
306
|
# FPS計測用
|
|
384
307
|
frame_times = []
|
|
385
308
|
last_frame_time = Time.now
|
|
@@ -426,54 +349,11 @@ module Rufio
|
|
|
426
349
|
end
|
|
427
350
|
needs_redraw = true if had_input
|
|
428
351
|
|
|
429
|
-
#
|
|
430
|
-
if @background_executor
|
|
431
|
-
if !@background_executor.running? && @background_executor.get_completion_message
|
|
432
|
-
completion_msg = @background_executor.get_completion_message
|
|
433
|
-
# 通知メッセージとして表示
|
|
434
|
-
notification_message = completion_msg
|
|
435
|
-
notification_time = start
|
|
436
|
-
# フッターのランプ表示用にも設定(UIRenderer が管理)
|
|
437
|
-
@ui_renderer.completion_lamp_message = completion_msg
|
|
438
|
-
@ui_renderer.completion_lamp_time = start
|
|
439
|
-
@background_executor.instance_variable_set(:@completion_message, nil) # メッセージをクリア
|
|
440
|
-
needs_redraw = true
|
|
441
|
-
end
|
|
442
|
-
last_notification_check = start
|
|
443
|
-
end
|
|
352
|
+
# 通知・ランプ・実行状態の更新を一か所に集約する。
|
|
353
|
+
needs_redraw = true if @background_status.update(@background_executor, now: start)
|
|
444
354
|
|
|
445
|
-
|
|
446
|
-
if @
|
|
447
|
-
current_running = @background_executor.running?
|
|
448
|
-
if @last_bg_running != current_running
|
|
449
|
-
@last_bg_running = current_running
|
|
450
|
-
needs_redraw = true
|
|
451
|
-
end
|
|
452
|
-
end
|
|
453
|
-
|
|
454
|
-
# 完了ランプの表示状態をチェック(0.5秒ごと)
|
|
455
|
-
if (start - last_lamp_check) > 0.5
|
|
456
|
-
current_lamp = @ui_renderer.completion_lamp_message
|
|
457
|
-
if current_lamp != previous_lamp_message
|
|
458
|
-
previous_lamp_message = current_lamp
|
|
459
|
-
needs_redraw = true
|
|
460
|
-
end
|
|
461
|
-
# 完了ランプのタイムアウトチェック
|
|
462
|
-
if @ui_renderer.completion_lamp_message && @ui_renderer.completion_lamp_time &&
|
|
463
|
-
(start - @ui_renderer.completion_lamp_time) >= 3.0
|
|
464
|
-
@ui_renderer.completion_lamp_message = nil
|
|
465
|
-
needs_redraw = true
|
|
466
|
-
end
|
|
467
|
-
last_lamp_check = start
|
|
468
|
-
end
|
|
469
|
-
|
|
470
|
-
# 通知メッセージの変化をチェック
|
|
471
|
-
current_notification = notification_message && (start - notification_time) < 3.0 ? notification_message : nil
|
|
472
|
-
if current_notification != previous_notification
|
|
473
|
-
previous_notification = current_notification
|
|
474
|
-
notification_message = nil if current_notification.nil?
|
|
475
|
-
needs_redraw = true
|
|
476
|
-
end
|
|
355
|
+
needs_redraw = true if @global_search.poll
|
|
356
|
+
needs_redraw = true if @keybind_handler&.poll_archive_jobs
|
|
477
357
|
|
|
478
358
|
# 非同期シンタックスハイライト完了チェック(UIRenderer側のフラグを参照)
|
|
479
359
|
if @ui_renderer.highlight_updated?
|
|
@@ -490,11 +370,7 @@ module Rufio
|
|
|
490
370
|
# DRAW & RENDER phase - 変更があった場合のみ描画
|
|
491
371
|
if needs_redraw
|
|
492
372
|
# Screenバッファに描画(clearは呼ばない。必要な部分だけ更新)
|
|
493
|
-
|
|
494
|
-
draw_screen_to_buffer(@screen, notification_message, current_fps)
|
|
495
|
-
else
|
|
496
|
-
draw_screen_to_buffer(@screen, nil, current_fps)
|
|
497
|
-
end
|
|
373
|
+
draw_screen_to_buffer(@screen, @background_status.message, current_fps)
|
|
498
374
|
|
|
499
375
|
# ヘルプまたはコマンドモードのダイアログをオーバーレイに描画
|
|
500
376
|
if help_dialog_active?
|
|
@@ -529,6 +405,12 @@ module Rufio
|
|
|
529
405
|
|
|
530
406
|
# UIRenderer に全描画処理を委譲
|
|
531
407
|
def draw_screen_to_buffer(screen, notification_message = nil, fps = nil)
|
|
408
|
+
if global_search_active?
|
|
409
|
+
@global_search.draw(screen)
|
|
410
|
+
@ui_renderer.draw_mode_tabs_to_buffer(screen, @screen_height - 1)
|
|
411
|
+
return
|
|
412
|
+
end
|
|
413
|
+
|
|
532
414
|
@ui_renderer.draw_screen_to_buffer(
|
|
533
415
|
screen, notification_message, fps,
|
|
534
416
|
in_job_mode: @in_job_mode,
|
|
@@ -598,14 +480,14 @@ module Rufio
|
|
|
598
480
|
return true
|
|
599
481
|
end
|
|
600
482
|
input = case third_char
|
|
601
|
-
when 'A' then 'k' # Up arrow
|
|
602
|
-
when 'B' then 'j' # Down arrow
|
|
603
|
-
when 'C' then 'l' # Right arrow
|
|
604
|
-
when 'D' then 'h' # Left arrow
|
|
483
|
+
when 'A' then global_search_active? && !help_dialog_active? ? :up : 'k' # Up arrow
|
|
484
|
+
when 'B' then global_search_active? && !help_dialog_active? ? :down : 'j' # Down arrow
|
|
485
|
+
when 'C' then global_search_active? && !help_dialog_active? ? :right : 'l' # Right arrow
|
|
486
|
+
when 'D' then global_search_active? && !help_dialog_active? ? :left : 'h' # Left arrow
|
|
605
487
|
when 'Z'
|
|
606
488
|
if help_dialog_active?
|
|
607
489
|
handle_help_input("\e[Z")
|
|
608
|
-
|
|
490
|
+
elsif !global_search_active?
|
|
609
491
|
handle_shift_tab
|
|
610
492
|
end
|
|
611
493
|
return true # Shift+Tab
|
|
@@ -621,15 +503,16 @@ module Rufio
|
|
|
621
503
|
end
|
|
622
504
|
|
|
623
505
|
return handle_help_input(input) if help_dialog_active?
|
|
506
|
+
return handle_global_search_input(input) if global_search_active?
|
|
624
507
|
|
|
625
508
|
# TabキーはFilesモードの時のみブックマーク循環移動(ジョブモード中は無効)
|
|
626
|
-
if input == "\t" && @tab_mode_manager.current_mode == :files && !@in_job_mode
|
|
509
|
+
if input == "\t" && (@tab_mode_manager.current_mode == :files || @keybind_handler.config_mode?) && !@in_job_mode
|
|
627
510
|
handle_tab_key
|
|
628
511
|
return true
|
|
629
512
|
end
|
|
630
513
|
|
|
631
514
|
# 数字キー(1-9): Filesモード かつ フィルターモード外でブックマークジャンプ+ハイライト
|
|
632
|
-
if input&.match?(/^[1-9]$/) && @tab_mode_manager.current_mode == :files && !@keybind_handler.filter_active?
|
|
515
|
+
if input&.match?(/^[1-9]$/) && (@tab_mode_manager.current_mode == :files || @keybind_handler.config_mode?) && !@keybind_handler.filter_active?
|
|
633
516
|
handle_bookmark_key(input.to_i)
|
|
634
517
|
return true
|
|
635
518
|
end
|
|
@@ -656,6 +539,12 @@ module Rufio
|
|
|
656
539
|
@running = false
|
|
657
540
|
end
|
|
658
541
|
|
|
542
|
+
# T キー: 確認なしで終了し、現在のディレクトリを終了後の移動先にする
|
|
543
|
+
if result == :quit_cd
|
|
544
|
+
@exit_directory = @directory_listing.current_path
|
|
545
|
+
@running = false
|
|
546
|
+
end
|
|
547
|
+
|
|
659
548
|
# 入力があったことを返す
|
|
660
549
|
true
|
|
661
550
|
end
|
|
@@ -682,7 +571,7 @@ module Rufio
|
|
|
682
571
|
|
|
683
572
|
# Shift+Tab: Filesモードでは前のブックマークへ循環移動、それ以外はモード逆順切り替え
|
|
684
573
|
def handle_shift_tab
|
|
685
|
-
if @tab_mode_manager.current_mode == :files
|
|
574
|
+
if @tab_mode_manager.current_mode == :files || @keybind_handler.config_mode?
|
|
686
575
|
prev_idx = @keybind_handler.goto_prev_bookmark
|
|
687
576
|
if prev_idx
|
|
688
577
|
@ui_renderer.set_highlighted_bookmark(prev_idx + 1)
|
|
@@ -710,6 +599,12 @@ module Rufio
|
|
|
710
599
|
return
|
|
711
600
|
end
|
|
712
601
|
|
|
602
|
+
if global_search_active?
|
|
603
|
+
handle_global_search_input(:up) if btn == 64
|
|
604
|
+
handle_global_search_input(:down) if btn == 65
|
|
605
|
+
return
|
|
606
|
+
end
|
|
607
|
+
|
|
713
608
|
case btn
|
|
714
609
|
when 0 # 左クリック(押下のみ処理)
|
|
715
610
|
handle_mouse_left_click(col, row) if press
|
|
@@ -739,13 +634,14 @@ module Rufio
|
|
|
739
634
|
|
|
740
635
|
# ファイルリストのクリック行からエントリを選択する(ダブルクリック対応)
|
|
741
636
|
def handle_mouse_file_click(row)
|
|
742
|
-
#
|
|
743
|
-
|
|
744
|
-
|
|
637
|
+
# コンテンツ行はターミナル行(1 始まり)で CONTENT_START_LINE+1 〜 screen_height-1
|
|
638
|
+
first_row = CONTENT_START_LINE + 1
|
|
639
|
+
content_height = @screen_height - HEADER_FOOTER_MARGIN
|
|
640
|
+
return unless row >= first_row && row <= @screen_height - 1
|
|
745
641
|
|
|
746
642
|
current_idx = @keybind_handler.current_index
|
|
747
643
|
start_index = [current_idx - content_height / 2, 0].max
|
|
748
|
-
target_index = start_index + (row -
|
|
644
|
+
target_index = start_index + (row - first_row)
|
|
749
645
|
|
|
750
646
|
entries = @keybind_handler.send(:get_active_entries)
|
|
751
647
|
return unless target_index >= 0 && target_index < entries.length
|
|
@@ -791,33 +687,7 @@ module Rufio
|
|
|
791
687
|
|
|
792
688
|
# モード変更を適用
|
|
793
689
|
def apply_mode_change(mode)
|
|
794
|
-
|
|
795
|
-
when :files
|
|
796
|
-
# ヘルプモードまたはログビューワモードから戻る
|
|
797
|
-
if @keybind_handler.help_mode?
|
|
798
|
-
@keybind_handler.send(:exit_help_mode)
|
|
799
|
-
elsif @keybind_handler.log_viewer_mode?
|
|
800
|
-
@keybind_handler.send(:exit_log_viewer_mode)
|
|
801
|
-
elsif @keybind_handler.in_job_mode?
|
|
802
|
-
@keybind_handler.send(:exit_job_mode)
|
|
803
|
-
end
|
|
804
|
-
when :help
|
|
805
|
-
show_help_dialog
|
|
806
|
-
when :logs
|
|
807
|
-
# ログビューワモードに入る
|
|
808
|
-
unless @keybind_handler.log_viewer_mode?
|
|
809
|
-
@keybind_handler.send(:exit_help_mode) if @keybind_handler.help_mode?
|
|
810
|
-
@keybind_handler.send(:exit_job_mode) if @keybind_handler.in_job_mode?
|
|
811
|
-
@keybind_handler.send(:enter_log_viewer_mode)
|
|
812
|
-
end
|
|
813
|
-
when :jobs
|
|
814
|
-
# ジョブモードに入る
|
|
815
|
-
unless @keybind_handler.in_job_mode?
|
|
816
|
-
@keybind_handler.send(:exit_help_mode) if @keybind_handler.help_mode?
|
|
817
|
-
@keybind_handler.send(:exit_log_viewer_mode) if @keybind_handler.log_viewer_mode?
|
|
818
|
-
@keybind_handler.enter_job_mode
|
|
819
|
-
end
|
|
820
|
-
end
|
|
690
|
+
@keybind_handler.switch_mode(mode)
|
|
821
691
|
end
|
|
822
692
|
|
|
823
693
|
# コマンドモード関連のメソッドは public にする
|
|
@@ -893,22 +763,9 @@ module Rufio
|
|
|
893
763
|
@command_input = ""
|
|
894
764
|
when "\e"
|
|
895
765
|
# Escape キーでコマンドモードをキャンセル
|
|
896
|
-
# まずコマンドウィンドウをクリア
|
|
897
766
|
@command_mode_ui.clear_prompt
|
|
898
767
|
deactivate_command_mode
|
|
899
|
-
|
|
900
|
-
if @screen && @renderer
|
|
901
|
-
# レンダラーの前フレーム情報をリセット(差分レンダリングを強制的に全体描画にする)
|
|
902
|
-
@renderer.clear
|
|
903
|
-
@screen.clear
|
|
904
|
-
draw_screen_to_buffer(@screen, nil, nil)
|
|
905
|
-
@renderer.render(@screen)
|
|
906
|
-
# カーソルを画面外に移動(メインループと同じ処理)
|
|
907
|
-
print "\e[#{@screen_height};#{@screen_width}H"
|
|
908
|
-
else
|
|
909
|
-
# フォールバック(古い実装)
|
|
910
|
-
draw_screen
|
|
911
|
-
end
|
|
768
|
+
render_full_screen
|
|
912
769
|
when "\t"
|
|
913
770
|
# Tab キーで補完
|
|
914
771
|
handle_tab_completion
|
|
@@ -1035,16 +892,6 @@ module Rufio
|
|
|
1035
892
|
info_notice.mark_as_shown(notice[:file])
|
|
1036
893
|
end
|
|
1037
894
|
|
|
1038
|
-
# ログモードに入る(廃止済み: 空のメソッド)
|
|
1039
|
-
def enter_log_mode(_project_log)
|
|
1040
|
-
# プロジェクトモード廃止により何もしない
|
|
1041
|
-
end
|
|
1042
|
-
|
|
1043
|
-
# ログモードを終了(廃止済み: 空のメソッド)
|
|
1044
|
-
def exit_log_mode
|
|
1045
|
-
# プロジェクトモード廃止により何もしない
|
|
1046
|
-
end
|
|
1047
|
-
|
|
1048
895
|
# ジョブモードを設定
|
|
1049
896
|
def set_job_mode(job_mode, job_manager, notification_manager)
|
|
1050
897
|
@job_mode_instance = job_mode
|
|
@@ -1062,17 +909,8 @@ module Rufio
|
|
|
1062
909
|
@in_job_mode = false
|
|
1063
910
|
@job_mode_instance = nil
|
|
1064
911
|
@job_manager = nil
|
|
1065
|
-
# バッファベースの全画面再描画を使用
|
|
1066
912
|
update_screen_size
|
|
1067
|
-
|
|
1068
|
-
@renderer.clear
|
|
1069
|
-
@screen.clear
|
|
1070
|
-
draw_screen_to_buffer(@screen, nil, nil)
|
|
1071
|
-
@renderer.render(@screen)
|
|
1072
|
-
print "\e[#{@screen_height};#{@screen_width}H"
|
|
1073
|
-
else
|
|
1074
|
-
draw_screen
|
|
1075
|
-
end
|
|
913
|
+
render_full_screen
|
|
1076
914
|
end
|
|
1077
915
|
|
|
1078
916
|
# ジョブモード再描画をトリガー
|
|
@@ -1080,70 +918,6 @@ module Rufio
|
|
|
1080
918
|
@job_mode_needs_redraw = true
|
|
1081
919
|
end
|
|
1082
920
|
|
|
1083
|
-
# ジョブモード画面を描画(バッファベース描画への橋渡し)
|
|
1084
|
-
def draw_job_mode_screen
|
|
1085
|
-
return unless @in_job_mode && @job_mode_instance && @job_manager
|
|
1086
|
-
return unless @screen && @renderer
|
|
1087
|
-
|
|
1088
|
-
# バッファベースの描画を使用
|
|
1089
|
-
draw_screen_to_buffer(@screen, nil, nil)
|
|
1090
|
-
@renderer.render(@screen)
|
|
1091
|
-
print "\e[#{@screen_height};#{@screen_width}H"
|
|
1092
|
-
|
|
1093
|
-
STDOUT.flush
|
|
1094
|
-
@job_mode_needs_redraw = false
|
|
1095
|
-
end
|
|
1096
|
-
|
|
1097
|
-
# Noice風の通知を描画
|
|
1098
|
-
def draw_notifications
|
|
1099
|
-
nm = @notification_manager || @keybind_handler&.notification_manager
|
|
1100
|
-
return unless nm
|
|
1101
|
-
|
|
1102
|
-
# 期限切れの通知を削除
|
|
1103
|
-
nm.expire_old_notifications
|
|
1104
|
-
|
|
1105
|
-
notifications = nm.notifications
|
|
1106
|
-
return if notifications.empty?
|
|
1107
|
-
|
|
1108
|
-
# 通知の幅と位置
|
|
1109
|
-
notification_width = 22
|
|
1110
|
-
x = @screen_width - notification_width - 2 # 右端から2文字マージン
|
|
1111
|
-
|
|
1112
|
-
notifications.each_with_index do |notif, i|
|
|
1113
|
-
y = 2 + (i * 5) # 各通知4行 + 間隔1行
|
|
1114
|
-
|
|
1115
|
-
# 色設定
|
|
1116
|
-
border_color = notif[:border_color] == :green ? "\e[32m" : "\e[31m"
|
|
1117
|
-
reset = "\e[0m"
|
|
1118
|
-
|
|
1119
|
-
# ステータスアイコン
|
|
1120
|
-
icon = notif[:type] == :success ? '✓' : '✗'
|
|
1121
|
-
|
|
1122
|
-
# 通知の内容を作成
|
|
1123
|
-
name_line = "#{icon} #{notif[:name]}"[0...notification_width - 4]
|
|
1124
|
-
status_line = notif[:status_text][0...notification_width - 4]
|
|
1125
|
-
|
|
1126
|
-
# 上部ボーダー
|
|
1127
|
-
print "\e[#{y};#{x}H#{border_color}╭#{'─' * (notification_width - 2)}╮#{reset}"
|
|
1128
|
-
|
|
1129
|
-
# 1行目: アイコン + 名前
|
|
1130
|
-
print "\e[#{y + 1};#{x}H#{border_color}│#{reset} #{name_line.ljust(notification_width - 4)} #{border_color}│#{reset}"
|
|
1131
|
-
|
|
1132
|
-
# 2行目: ステータス
|
|
1133
|
-
print "\e[#{y + 2};#{x}H#{border_color}│#{reset} #{status_line.ljust(notification_width - 6)} #{border_color}│#{reset}"
|
|
1134
|
-
|
|
1135
|
-
# Exit code行(失敗時のみ)
|
|
1136
|
-
if notif[:type] == :error && notif[:exit_code]
|
|
1137
|
-
exit_line = "Exit code: #{notif[:exit_code]}"[0...notification_width - 6]
|
|
1138
|
-
print "\e[#{y + 3};#{x}H#{border_color}│#{reset} #{exit_line.ljust(notification_width - 6)} #{border_color}│#{reset}"
|
|
1139
|
-
print "\e[#{y + 4};#{x}H#{border_color}╰#{'─' * (notification_width - 2)}╯#{reset}"
|
|
1140
|
-
else
|
|
1141
|
-
# 下部ボーダー
|
|
1142
|
-
print "\e[#{y + 3};#{x}H#{border_color}╰#{'─' * (notification_width - 2)}╯#{reset}"
|
|
1143
|
-
end
|
|
1144
|
-
end
|
|
1145
|
-
end
|
|
1146
|
-
|
|
1147
921
|
# オーバーレイダイアログを表示してキー入力を待つヘルパーメソッド
|
|
1148
922
|
# @param title [String] ダイアログタイトル
|
|
1149
923
|
# @param content_lines [Array<String>] コンテンツ行
|
|
@@ -1190,7 +964,7 @@ module Rufio
|
|
|
1190
964
|
@renderer.render(@screen)
|
|
1191
965
|
|
|
1192
966
|
# キー入力を待つ
|
|
1193
|
-
key = block_given? ? yield :
|
|
967
|
+
key = block_given? ? yield : KeyInput.getch
|
|
1194
968
|
|
|
1195
969
|
# オーバーレイを無効化
|
|
1196
970
|
@screen.disable_overlay
|
|
@@ -1204,6 +978,44 @@ module Rufio
|
|
|
1204
978
|
# Screen と Renderer のアクセサ(他のクラスから利用可能に)
|
|
1205
979
|
attr_reader :screen, :renderer
|
|
1206
980
|
|
|
981
|
+
def start_global_search(bookmarks)
|
|
982
|
+
@global_search.open(bookmarks)
|
|
983
|
+
end
|
|
984
|
+
|
|
985
|
+
def global_search_active?
|
|
986
|
+
@global_search.active?
|
|
987
|
+
end
|
|
988
|
+
|
|
989
|
+
def handle_global_search_input(input)
|
|
990
|
+
if input == ConfigLoader.keybinds[:help]
|
|
991
|
+
show_help_dialog
|
|
992
|
+
return true
|
|
993
|
+
end
|
|
994
|
+
result = @global_search.handle_key(input)
|
|
995
|
+
if result.is_a?(Hash) && File.file?(result[:path])
|
|
996
|
+
@keybind_handler.switch_mode(:files)
|
|
997
|
+
if @keybind_handler.navigate_to_directory(File.dirname(result[:path]))
|
|
998
|
+
index = @keybind_handler.get_active_entries.index { |entry| entry[:path] == result[:path] }
|
|
999
|
+
@keybind_handler.select_index(index) if index
|
|
1000
|
+
@keybind_handler.unfocus_preview_pane
|
|
1001
|
+
@global_search.close
|
|
1002
|
+
end
|
|
1003
|
+
end
|
|
1004
|
+
unless global_search_active?
|
|
1005
|
+
mode = if @keybind_handler.in_job_mode?
|
|
1006
|
+
:jobs
|
|
1007
|
+
elsif @keybind_handler.log_viewer_mode?
|
|
1008
|
+
:logs
|
|
1009
|
+
elsif @keybind_handler.config_mode?
|
|
1010
|
+
:config
|
|
1011
|
+
else
|
|
1012
|
+
:files
|
|
1013
|
+
end
|
|
1014
|
+
@tab_mode_manager.switch_to(mode)
|
|
1015
|
+
end
|
|
1016
|
+
true
|
|
1017
|
+
end
|
|
1018
|
+
|
|
1207
1019
|
# ヘルプは非同期オーバーレイとして表示し、元のモードを保持する。
|
|
1208
1020
|
def show_help_dialog
|
|
1209
1021
|
@help_menu.open
|
|
@@ -1229,6 +1041,17 @@ module Rufio
|
|
|
1229
1041
|
end
|
|
1230
1042
|
end
|
|
1231
1043
|
|
|
1044
|
+
# 全画面をバッファから再描画する
|
|
1045
|
+
def render_full_screen
|
|
1046
|
+
return unless @screen && @renderer
|
|
1047
|
+
|
|
1048
|
+
@renderer.clear
|
|
1049
|
+
@screen.clear
|
|
1050
|
+
draw_screen_to_buffer(@screen, nil, nil)
|
|
1051
|
+
@renderer.render(@screen)
|
|
1052
|
+
print "\e[#{@screen_height};#{@screen_width}H"
|
|
1053
|
+
end
|
|
1054
|
+
|
|
1232
1055
|
end
|
|
1233
1056
|
end
|
|
1234
1057
|
|
data/lib/rufio/text_utils.rb
CHANGED
|
@@ -134,7 +134,7 @@ module Rufio
|
|
|
134
134
|
line = line.encode('UTF-8', invalid: :replace, undef: :replace, replace: '?')
|
|
135
135
|
# Remove trailing whitespace
|
|
136
136
|
line = line.rstrip
|
|
137
|
-
rescue EncodingError, ArgumentError
|
|
137
|
+
rescue EncodingError, ArgumentError
|
|
138
138
|
# If encoding fails completely, skip this line
|
|
139
139
|
wrapped << '[encoding error]'
|
|
140
140
|
next
|
|
@@ -152,7 +152,7 @@ module Rufio
|
|
|
152
152
|
wrapped << line
|
|
153
153
|
next
|
|
154
154
|
end
|
|
155
|
-
rescue ArgumentError
|
|
155
|
+
rescue ArgumentError
|
|
156
156
|
# If display_width fails, just truncate by byte length
|
|
157
157
|
if line.bytesize <= max_width
|
|
158
158
|
wrapped << line
|
|
@@ -181,7 +181,7 @@ module Rufio
|
|
|
181
181
|
|
|
182
182
|
# Add remaining characters
|
|
183
183
|
wrapped << current_line.join unless current_line.empty?
|
|
184
|
-
rescue ArgumentError, EncodingError
|
|
184
|
+
rescue ArgumentError, EncodingError
|
|
185
185
|
# If character iteration fails, just add the line truncated
|
|
186
186
|
truncated = line.byteslice(0, [max_width, line.bytesize].min)
|
|
187
187
|
wrapped << (truncated || line)
|
|
@@ -190,5 +190,28 @@ module Rufio
|
|
|
190
190
|
|
|
191
191
|
wrapped
|
|
192
192
|
end
|
|
193
|
+
|
|
194
|
+
# Shorten a path by keeping the tail and prefixing with ellipsis if longer than max_length.
|
|
195
|
+
# Optionally replaces ENV['HOME'] / Dir.home with '~' when tilde: true.
|
|
196
|
+
def shorten_path_from_end(path, max_length, tilde: false, ellipsis: '...')
|
|
197
|
+
return path.to_s if max_length <= 0
|
|
198
|
+
|
|
199
|
+
display = path.to_s
|
|
200
|
+
if tilde
|
|
201
|
+
home = Dir.home rescue ENV['HOME']
|
|
202
|
+
if home && !home.empty?
|
|
203
|
+
if display == home
|
|
204
|
+
display = '~'
|
|
205
|
+
elsif display.start_with?(home + '/')
|
|
206
|
+
display = "~#{display.delete_prefix(home)}"
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
return display if display.length <= max_length
|
|
212
|
+
return display[-max_length..] || display if max_length <= ellipsis.length
|
|
213
|
+
|
|
214
|
+
"#{ellipsis}#{display[-(max_length - ellipsis.length)..]}"
|
|
215
|
+
end
|
|
193
216
|
end
|
|
194
217
|
end
|