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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +46 -0
  3. data/README.md +105 -2
  4. data/README_ja.md +103 -2
  5. data/Rakefile +14 -1
  6. data/bin/rufio +31 -2
  7. data/docs/fast_zip_design_ja.md +167 -0
  8. data/examples/config.rb +8 -0
  9. data/info/help.md +2 -0
  10. data/info/keybindings.md +3 -1
  11. data/info/welcome.md +1 -1
  12. data/lib/rufio/application.rb +12 -1
  13. data/lib/rufio/archive_controller.rb +211 -0
  14. data/lib/rufio/background_command_executor.rb +9 -0
  15. data/lib/rufio/background_status.rb +49 -0
  16. data/lib/rufio/bookmark_controller.rb +38 -121
  17. data/lib/rufio/bookmark_manager.rb +77 -331
  18. data/lib/rufio/command_completion.rb +6 -1
  19. data/lib/rufio/command_history.rb +5 -2
  20. data/lib/rufio/command_mode.rb +40 -134
  21. data/lib/rufio/command_mode_ui.rb +4 -42
  22. data/lib/rufio/config.rb +89 -137
  23. data/lib/rufio/config_loader.rb +21 -13
  24. data/lib/rufio/dialog_renderer.rb +64 -152
  25. data/lib/rufio/dialog_support.rb +69 -0
  26. data/lib/rufio/document_text_extractor.rb +167 -0
  27. data/lib/rufio/file_operation_controller.rb +49 -222
  28. data/lib/rufio/file_preview.rb +46 -2
  29. data/lib/rufio/global_search.rb +373 -0
  30. data/lib/rufio/help_menu.rb +27 -2
  31. data/lib/rufio/interpreter_resolver.rb +6 -11
  32. data/lib/rufio/job_manager.rb +2 -5
  33. data/lib/rufio/key_input.rb +77 -0
  34. data/lib/rufio/keybind_handler.rb +120 -112
  35. data/lib/rufio/local_script_scanner.rb +6 -18
  36. data/lib/rufio/native/aarch64-linux-gnu/librufio_zig.so +0 -0
  37. data/lib/rufio/native/aarch64-linux-musl/librufio_zig.so +0 -0
  38. data/lib/rufio/native/aarch64-macos/librufio_zig.dylib +0 -0
  39. data/lib/rufio/native/aarch64-windows/rufio_zig.dll +0 -0
  40. data/lib/rufio/native/x86_64-linux-gnu/librufio_zig.so +0 -0
  41. data/lib/rufio/native/x86_64-linux-musl/librufio_zig.so +0 -0
  42. data/lib/rufio/native/x86_64-macos/librufio_zig.dylib +0 -0
  43. data/lib/rufio/native/x86_64-windows/rufio_zig.dll +0 -0
  44. data/lib/rufio/native_content_search.rb +153 -0
  45. data/lib/rufio/native_scanner.rb +13 -68
  46. data/lib/rufio/native_scanner_zig.rb +106 -56
  47. data/lib/rufio/native_zip.rb +121 -0
  48. data/lib/rufio/navigation_controller.rb +18 -20
  49. data/lib/rufio/screen.rb +2 -12
  50. data/lib/rufio/script_executor.rb +6 -73
  51. data/lib/rufio/script_file.rb +44 -0
  52. data/lib/rufio/script_path_manager.rb +9 -43
  53. data/lib/rufio/script_runner.rb +13 -50
  54. data/lib/rufio/search_controller.rb +1 -1
  55. data/lib/rufio/shell_command_completion.rb +1 -5
  56. data/lib/rufio/shell_init.rb +90 -0
  57. data/lib/rufio/tab_mode_manager.rb +5 -1
  58. data/lib/rufio/task_status.rb +5 -1
  59. data/lib/rufio/terminal_ui.rb +116 -293
  60. data/lib/rufio/text_utils.rb +26 -3
  61. data/lib/rufio/ui_renderer.rb +68 -41
  62. data/lib/rufio/version.rb +1 -1
  63. data/lib/rufio/zip_archive.rb +314 -0
  64. data/lib/rufio/zip_extractor.rb +346 -0
  65. data/lib/rufio/zip_writer.rb +268 -0
  66. data/lib/rufio/zoxide_integration.rb +5 -43
  67. data/lib/rufio.rb +5 -0
  68. data/lib_zig/rufio_native/Makefile +54 -17
  69. data/lib_zig/rufio_native/bench.sh +36 -0
  70. data/lib_zig/rufio_native/build.zig +32 -35
  71. data/lib_zig/rufio_native/src/cli.zig +99 -0
  72. data/lib_zig/rufio_native/src/main.zig +187 -83
  73. data/lib_zig/rufio_native/src/os.zig +889 -0
  74. data/lib_zig/rufio_native/src/search.zig +919 -0
  75. data/lib_zig/rufio_native/src/search_test.zig +88 -0
  76. data/lib_zig/rufio_native/src/zip.zig +482 -0
  77. metadata +126 -3
  78. data/lib/rufio/native/rufio_zig.bundle +0 -0
@@ -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
- HEADER_PADDING = 2 # ヘッダーのパディング
22
- FILTER_TEXT_RESERVED = 15 # フィルタテキスト表示の予約幅
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
- # Bookmark highlight duration (seconds)
35
- BOOKMARK_HIGHLIGHT_DURATION = 0.5
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
- @preview_cache.clear
140
- @last_preview_path = nil
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 を使わずキューから取得(5ms タイムアウト)。
199
+ # Windows: IO.select を使わずキューから取得。
240
200
  def read_next_input_byte
241
201
  if windows?
242
- # ConPTY ではバイト到着が遅れることがあるため 5ms では ESC 単体と誤判定する
243
- b = windows_queue_pop_with_timeout(0.025)
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
- remaining = case first_byte_int
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
- if QUEUE_POP_TIMEOUT_SUPPORTED
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
- # バックグラウンドコマンドの完了チェック(0.1秒ごと)
430
- if @background_executor && (start - last_notification_check) > check_interval
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 @background_executor
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
- if notification_message && (start - notification_time) < 3.0
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
- else
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
- # コンテンツ行はターミナル行2〜(screen_height-1)
743
- content_height = @screen_height - 2
744
- return unless row >= 2 && row <= @screen_height - 1
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 - 2)
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
- case mode
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
- if @screen && @renderer
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 : STDIN.getch
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
 
@@ -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 => e
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 => e
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 => e
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