rufio 0.82.1 → 0.83.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 95f95e73aa18690cd88f4c18a1c1b4c7d5bccf7a07ba7c507cd569acdc3985e3
4
- data.tar.gz: c7a636b0fe589fcfa516d3109186e310132bdb478a0357d0542be76239dfc6fc
3
+ metadata.gz: c96ef480cab0968de6e595b56af26baeb59c0fd0542a85e2ccc0e4d1970bd696
4
+ data.tar.gz: 50366b86a8ef40df39f00a58e7a4e4d86d8314c919855acd96cdc0a03f39a156
5
5
  SHA512:
6
- metadata.gz: 5bfda6ac1bdd743ca4c54a5b5e6dc1c72614900ec280a8c972ae14e7effb1cb84d9d693ee4f74115595e5a68a7d8e81ec350a7fd03506ff413257c8d9a270066
7
- data.tar.gz: 1ab9a355aaf8d5be8f8b66458935886b210fbd2bf17519b1b6793644fe6f0f2f69ccacd5cd376986d0a1dd6be7d0692200da61080172db5a1b6db2c24fd6e6c7
6
+ metadata.gz: dd1d2a5f82340c12caf57184b90cf3ad0773ef905caf07214376eab6b41d2af5c99fec088bd6e4dc51c20b928c3640d62c8b1d3681d08724fdb32e719e4205cd
7
+ data.tar.gz: d38b805a6592cae4b6cc1c8e08ea587c3f88ebdadd605561b1cef67a3700009f5d656df61548a2f755db659c89b995dfabdd38a29e14c8e728280d22f12fe337
data/CHANGELOG.md CHANGED
@@ -5,6 +5,34 @@ All notable changes to rufio will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.83.1] - 2026-03-20
9
+
10
+ ### Changed
11
+ - **Windows console input detection rewritten using Win32 API**: Replaced the background thread + Queue workaround (introduced in v0.82.1 to handle ESC key detection on Windows) with `GetNumberOfConsoleInputEvents` via the Win32 API (`fiddle` stdlib)
12
+ - The previous approach had a race condition where the background `STDIN.read(1)` thread competed with `STDIN.getch` calls in dialogs, potentially dropping dialog input (including multi-byte/Japanese characters)
13
+ - Background thread, Queue, and `windows_read_next_byte` helper are removed
14
+ - `read_next_input_byte` is now a single unified implementation for both Windows and Unix (`read_nonblock`)
15
+ - Cygwin is excluded from the Windows path since it provides POSIX-compatible `IO.select`
16
+ - Falls back to `read_nonblock` if `Fiddle::DLError` occurs (no extra gem required — `fiddle` is a Ruby stdlib)
17
+
18
+ ## [0.83.0] - 2026-03-14
19
+
20
+ ### Added
21
+ - **Mouse input support**: Enabled SGR extended mouse reporting (`\e[?1003h\e[?1006h`) with click and scroll handling
22
+ - **Left click (file list)**: Move cursor to the clicked row
23
+ - **Left double-click (file list)**: Re-click within 0.5s opens the file (equivalent to Enter)
24
+ - **Left click (right panel)**: Toggle preview focus on/off
25
+ - **Wheel scroll (file list)**: Scroll up/down (equivalent to j/k)
26
+ - **Wheel scroll (preview focused)**: Scroll the preview panel
27
+ - Mouse reporting is disabled on exit via `cleanup_terminal` (`\e[?1003l\e[?1006l`)
28
+ - **Architecture documentation**: Added `docs/architecture_concurrency_ja_v0.82.1.md`
29
+ - Documents rufio's concurrency model (single-threaded UI + worker threads + child processes)
30
+ - Documents memory model characteristics (screen buffer, preview cache, job history)
31
+
32
+ ### Changed
33
+ - **`read_next_input_byte` helper**: Extracted common ESC sequence reading logic to reduce duplication
34
+ - **`UIRenderer#left_panel_ratio` exposed**: Added `attr_reader` for use in mouse click panel detection
35
+
8
36
  ## [0.82.1] - 2026-03-07
9
37
 
10
38
  ### Fixed
@@ -0,0 +1,169 @@
1
+ # rufio 並行実行アーキテクチャ
2
+
3
+ ## 結論(Single Thread / Thread / Process / Event Loop)
4
+
5
+ rufio は **単一のメインプロセス**で動作し、UIは **シングルスレッドのイベントループ**で駆動されます。
6
+ ただし、実行中に以下を併用します。
7
+
8
+ - `Thread`: バックグラウンド実行、非同期スキャン、非同期ハイライト、Windows入力補助
9
+ - `Process`(子プロセス): `Open3` / `IO.popen` / `system` による外部コマンド実行
10
+
11
+ つまり実態は、**「シングルスレッドUI + 補助スレッド + 外部プロセス」構成**です。
12
+
13
+ ## 1. 起動とプロセス境界
14
+
15
+ - エントリポイントは [`bin/rufio`](../bin/rufio)
16
+ - 本体は [`Rufio::Application`](../lib/rufio/application.rb) が初期化し、[`TerminalUI#main_loop`](../lib/rufio/terminal_ui.rb) に入る
17
+ - アプリ自身は `fork`/常駐ワーカープロセスを持たない(コード上 `fork` 使用なし)
18
+
19
+ ### 子プロセスを作る経路
20
+
21
+ - `Open3.capture3` / `Open3.popen3`
22
+ - [`lib/rufio/background_command_executor.rb`](../lib/rufio/background_command_executor.rb)
23
+ - [`lib/rufio/command_mode.rb`](../lib/rufio/command_mode.rb)
24
+ - [`lib/rufio/script_runner.rb`](../lib/rufio/script_runner.rb)
25
+ - [`lib/rufio/script_executor.rb`](../lib/rufio/script_executor.rb)
26
+ - `IO.popen`(`bat` 実行)
27
+ - [`lib/rufio/syntax_highlighter.rb`](../lib/rufio/syntax_highlighter.rb)
28
+ - `system`(`tput`, `which` 等)
29
+ - [`lib/rufio/terminal_ui.rb`](../lib/rufio/terminal_ui.rb)
30
+ - [`lib/rufio/syntax_highlighter.rb`](../lib/rufio/syntax_highlighter.rb)
31
+
32
+ ## 2. Event Loop(UIの中心)
33
+
34
+ メインループは [`TerminalUI#main_loop`](../lib/rufio/terminal_ui.rb) で実装されています。
35
+
36
+ - ループ周期の基準: 約30FPS(`min_sleep_interval = 0.0333`)
37
+ - フェーズ: `UPDATE -> DRAW -> RENDER -> SLEEP`
38
+ - 入力: ノンブロッキング
39
+ - Unix: `IO.select(..., timeout=0)` + `STDIN.read_nonblock`
40
+ - Windows: 補助入力スレッド + `Queue` 受信
41
+ - 定期監視
42
+ - バックグラウンドコマンド完了チェック(約0.1秒周期)
43
+ - 通知ランプ期限チェック
44
+ - 非同期ハイライト完了フラグチェック
45
+
46
+ ポイントは「**UIスレッドはブロックしない**」ことで、重い処理は別スレッド/別プロセスへ逃がしています。
47
+
48
+ ## 3. Thread モデル
49
+
50
+ ### 3.1 常時/都度の補助スレッド
51
+
52
+ - バックグラウンドコマンド
53
+ - [`BackgroundCommandExecutor`](../lib/rufio/background_command_executor.rb)
54
+ - `Thread.new` で1ジョブ実行(同時1本に制限)
55
+ - スクリプトジョブ
56
+ - [`ScriptRunner`](../lib/rufio/script_runner.rb)
57
+ - [`CommandMode`](../lib/rufio/command_mode.rb)
58
+ - ジョブごとに `Thread.new` で実行
59
+ - 非同期ディレクトリスキャン
60
+ - [`NativeScannerRubyCore`](../lib/rufio/native_scanner.rb)
61
+ - スキャンごとに1スレッド
62
+ - 並列スキャン
63
+ - [`ParallelScanner`](../lib/rufio/parallel_scanner.rb)
64
+ - `Queue` + ワーカースレッドプール(既定4)
65
+ - 非同期シンタックスハイライト
66
+ - [`SyntaxHighlighter#highlight_async`](../lib/rufio/syntax_highlighter.rb)
67
+ - `Thread` + `Mutex` + pendingガード
68
+ - Windows入力補助
69
+ - [`TerminalUI#setup_terminal`](../lib/rufio/terminal_ui.rb)
70
+ - `STDIN.read(1)` を読む専用スレッド
71
+
72
+ ### 3.2 スレッド安全性の扱い
73
+
74
+ - `Mutex` で共有状態を保護(例: `NativeScannerRubyCore`, `SyntaxHighlighter`)
75
+ - `Queue` で producer/consumer 連携(`ParallelScanner`, Windows入力)
76
+ - UI反映はメインループでポーリング/フラグ監視し、描画系の責務を集中
77
+
78
+ ## 4. Process モデル
79
+
80
+ 外部コマンド実行は Ruby プロセス内で直接処理せず、子プロセスに委譲しています。
81
+
82
+ - シェルコマンド・スクリプト・rake は `Open3` 経由で実行
83
+ - タイムアウト付き実行では `Process.kill("TERM"/"KILL")` を使用
84
+ - [`ScriptExecutor`](../lib/rufio/script_executor.rb)
85
+ - ハイライトは `bat` 子プロセスから出力取得
86
+
87
+ このため、重い外部処理があっても UI スレッド停止を避けやすい構造です。
88
+
89
+ ## 5. Single Thread か?への回答
90
+
91
+ 質問に対しては次の回答が正確です。
92
+
93
+ - UI制御: **シングルスレッド(イベントループ)**
94
+ - 並行処理: **マルチスレッドを使用**
95
+ - 実処理実行: **外部子プロセスを多用**
96
+ - アーキテクチャ全体: **単一メインプロセス + イベントループ + ワーカースレッド + 子プロセス**
97
+
98
+ ## 6. 補足(運用上の含意)
99
+
100
+ - RubyスレッドはI/O待ちや外部プロセス待ちの分離には有効
101
+ - CPUバウンド処理をRubyスレッドで増やしても、処理系制約(GVL)で伸びにくい可能性がある
102
+ - 現状設計は「UI応答性優先」「外部コマンド委譲型」のTUIとして合理的
103
+
104
+ ## 7. メモリモデル
105
+
106
+ rufio のメモリは、概ね次の4種類で構成されます。
107
+
108
+ - 固定サイズの描画バッファ(画面サイズ依存)
109
+ - ディレクトリ/プレビューなどの作業データ(操作対象依存)
110
+ - キャッシュ/履歴(セッション経過で増える可能性あり)
111
+ - 外部プロセス実行時の一時データ(子プロセス出力依存)
112
+
113
+ ### 7.1 オブジェクトの寿命
114
+
115
+ - プロセス寿命で生存(起動時作成)
116
+ - `TerminalUI`, `UIRenderer`, `KeybindHandler`, `JobManager` など
117
+ - 画面更新ごとに再生成される一時データ
118
+ - 行描画文字列、差分描画バッファ
119
+ - ジョブ/コマンド実行中のみ生存
120
+ - ワーカースレッド、`Open3` の入出力文字列、スキャナーインスタンス
121
+
122
+ ### 7.2 固定上限に近い領域
123
+
124
+ - ダブルバッファ
125
+ - [`Screen`](../lib/rufio/screen.rb): `@cells`(`height x width`)
126
+ - [`Renderer`](../lib/rufio/renderer.rb): `@front`(`height` 行ぶんの表示文字列)
127
+ - これらは基本的に「端末サイズに比例」し、無制限には伸びない
128
+
129
+ ### 7.3 変動するが上限管理される領域
130
+
131
+ - 通知
132
+ - [`NotificationManager`](../lib/rufio/notification_manager.rb) は最大3件
133
+ - コマンド履歴(メモリ内)
134
+ - [`CommandHistory`](../lib/rufio/command_history.rb) は `max_size`(既定1000)
135
+ - ディレクトリエントリ
136
+ - [`DirectoryListing`](../lib/rufio/directory_listing.rb) は `refresh` ごとに `@entries` を再構築
137
+
138
+ ### 7.4 増加しやすい領域(要注意)
139
+
140
+ - プレビューキャッシュ
141
+ - [`UIRenderer`](../lib/rufio/ui_renderer.rb) の `@preview_cache` は閲覧ファイルパス単位で蓄積
142
+ - `wrapped` / `highlighted_wrapped` も幅ごとに追加される
143
+ - `clear_preview_cache` は実装されているが、通常フローからは呼ばれていない
144
+ - ジョブ一覧
145
+ - [`JobManager`](../lib/rufio/job_manager.rb) の `@jobs` は追加され続ける
146
+ - `clear_completed` はあるが、通常キー操作には接続されていない
147
+ - ジョブログ
148
+ - [`TaskStatus`](../lib/rufio/task_status.rb) の `@logs` は配列で蓄積(上限なし)
149
+ - 標準出力/標準エラーが大きいジョブはメモリ圧迫要因になる
150
+ - ログファイル(ディスク)
151
+ - [`CommandLogger`](../lib/rufio/command_logger.rb) はログファイルを増やし続ける
152
+ - `cleanup_old_logs` は実装済みだが、通常フローで自動実行はされない
153
+
154
+ ### 7.5 外部プロセス実行時のメモリ特性
155
+
156
+ - `Open3.capture3` は stdout/stderr を文字列として一括保持するため、
157
+ 出力が大きいほど Ruby ヒープ使用量が一時的に増える
158
+ - ただし実処理は子プロセス側で実行されるため、計算本体のメモリは原則として別プロセスに分離される
159
+
160
+ ### 7.6 Ruby/C 境界(Zig スキャナー)
161
+
162
+ - Zig経路では [`NativeScannerZigCore`](../lib/rufio/native_scanner_zig.rb) がハンドルを保持し、
163
+ `close` で `core_async_destroy` を呼んで明示解放する
164
+ - `scan` 呼び出し側は `ensure` で `close` しており、通常経路でハンドルリークしにくい設計
165
+
166
+ ### 7.7 まとめ
167
+
168
+ メモリモデルとしては、**描画バッファは安定**している一方で、**プレビューキャッシュ・ジョブ履歴・ジョブログ**は
169
+ セッションが長くなるほど増えうる構造です。長時間運用時のメモリ安定性は、この3点の上限管理の有無に最も左右されます。
@@ -185,22 +185,9 @@ module Rufio
185
185
  STDIN.raw!
186
186
  end
187
187
 
188
- # Windows: IO.select + read_nonblockはコンソールハンドルのESCキーを
189
- # 検出できない場合がある。バックグラウンドスレッドでSTDINを読み取り
190
- # Queueに格納することで信頼性のある入力検出を実現する。
191
- if windows?
192
- @windows_input_queue = Queue.new
193
- @windows_input_thread = Thread.new do
194
- loop do
195
- begin
196
- byte = STDIN.read(1)
197
- @windows_input_queue << byte if byte
198
- rescue
199
- break
200
- end
201
- end
202
- end
203
- end
188
+ # SGR拡張マウスレポートを有効化(ボタン + ホイール + 任意位置クリック)
189
+ print "\e[?1003h\e[?1006h"
190
+ STDOUT.flush
204
191
 
205
192
  # re-acquire terminal size (just in case)
206
193
  update_screen_size
@@ -213,22 +200,37 @@ module Rufio
213
200
  @screen_width, @screen_height = console.winsize.reverse
214
201
  end
215
202
 
203
+ # Cygwinは POSIX互換のIO.selectが使えるため除外
216
204
  def windows?
217
- RUBY_PLATFORM =~ /mswin|mingw|cygwin/ ? true : false
205
+ RUBY_PLATFORM =~ /mswin|mingw/ ? true : false
218
206
  end
219
207
 
220
- # Windows: エスケープシーケンスの後続バイトをQueueから短いタイムアウトで読み取る
221
- # VT対応ターミナルでの矢印キー(\e[A 等)の後続バイト読み取りに使用
222
- def windows_read_next_byte
223
- deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 0.005 # 5ms
224
- loop do
225
- begin
226
- return @windows_input_queue.pop(true)
227
- rescue ThreadError
228
- return nil if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
229
- sleep 0.001
230
- end
231
- end
208
+ # Windows: GetNumberOfConsoleInputEvents でコンソール入力バッファを確認する。
209
+ # IO.select はWindowsコンソールハンドルでESCキーを取りこぼすため使用しない。
210
+ # fiddle はRuby標準ライブラリなので追加gemは不要。
211
+ def windows_console_input_available?
212
+ require 'fiddle'
213
+ @win32_kernel32 ||= Fiddle.dlopen('kernel32')
214
+ @win32_get_std_handle ||= Fiddle::Function.new(
215
+ @win32_kernel32['GetStdHandle'],
216
+ [Fiddle::TYPE_INT], Fiddle::TYPE_VOIDP
217
+ )
218
+ @win32_get_num_events ||= Fiddle::Function.new(
219
+ @win32_kernel32['GetNumberOfConsoleInputEvents'],
220
+ [Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP], Fiddle::TYPE_INT
221
+ )
222
+ handle = @win32_get_std_handle.call(0xFFFFFFF6) # STD_INPUT_HANDLE = (DWORD)(-10)
223
+ count_ptr = Fiddle::Pointer.malloc(4)
224
+ @win32_get_num_events.call(handle, count_ptr)
225
+ count_ptr[0, 4].unpack1('L') > 0
226
+ rescue Fiddle::DLError
227
+ # fiddle が使えない場合は常に入力ありとみなし read_nonblock に任せる
228
+ true
229
+ end
230
+
231
+ # エスケープシーケンスの後続バイトを読み取る(Windows/Unix共通ヘルパー)
232
+ def read_next_input_byte
233
+ STDIN.read_nonblock(1) rescue nil
232
234
  end
233
235
 
234
236
  def cleanup_terminal
@@ -237,12 +239,9 @@ module Rufio
237
239
  STDIN.cooked!
238
240
  end
239
241
 
240
- # Windowsバックグラウンド入力スレッドを停止
241
- if @windows_input_thread
242
- @windows_input_thread.kill rescue nil
243
- @windows_input_thread = nil
244
- @windows_input_queue = nil
245
- end
242
+ # マウスレポートを無効化
243
+ print "\e[?1003l\e[?1006l"
244
+ STDOUT.flush
246
245
 
247
246
  system('tput rmcup') # normal screen
248
247
  system('tput cnorm') # cursor normal
@@ -425,31 +424,24 @@ module Rufio
425
424
  private
426
425
 
427
426
  # ノンブロッキング入力処理(ゲームループ用)
428
- # Windows: Queueベース、Unix: IO.select + read_nonblock
427
+ # Windows: GetNumberOfConsoleInputEvents で入力確認後 read_nonblock
428
+ # Unix: IO.select(timeout=0) で入力確認後 read_nonblock
429
429
  def handle_input_nonblocking
430
430
  # 入力バイトを1つ読み取る
431
- if @windows_input_queue
432
- # Windows: バックグラウンドスレッドのQueueからノンブロッキングで読み取る
433
- begin
434
- input = @windows_input_queue.pop(true)
435
- rescue ThreadError
436
- return false
437
- end
431
+ if windows?
432
+ # Windows: IO.selectはESCキーを取りこぼすため Win32 API で入力確認
433
+ return false unless windows_console_input_available?
438
434
  else
439
435
  # Unix: 0msタイムアウトで即座にチェック(30FPS = 33.33ms/frame)
440
- ready = IO.select([STDIN], nil, nil, 0)
441
- return false unless ready
442
-
443
- begin
444
- # read_nonblockを使ってノンブロッキングで1文字読み取る
445
- input = STDIN.read_nonblock(1)
446
- rescue IO::WaitReadable, IO::EAGAINWaitReadable
447
- # 入力が利用できない
448
- return false
449
- rescue Errno::ENOTTY, Errno::ENODEV
450
- # ターミナルでない環境
451
- return false
452
- end
436
+ return false unless IO.select([STDIN], nil, nil, 0)
437
+ end
438
+
439
+ begin
440
+ input = STDIN.read_nonblock(1)
441
+ rescue IO::WaitReadable, IO::EAGAINWaitReadable
442
+ return false
443
+ rescue Errno::ENOTTY, Errno::ENODEV
444
+ return false
453
445
  end
454
446
 
455
447
  # コマンドモードがアクティブな場合は、エスケープシーケンス処理をスキップ
@@ -461,17 +453,27 @@ module Rufio
461
453
 
462
454
  # 特殊キーの処理(エスケープシーケンス)(コマンドモード外のみ)
463
455
  if input == "\e"
464
- next_char = if @windows_input_queue
465
- windows_read_next_byte
466
- else
467
- STDIN.read_nonblock(1) rescue nil
468
- end
456
+ next_char = read_next_input_byte
469
457
  if next_char == '['
470
- # 矢印キーなどのシーケンス
471
- third_char = if @windows_input_queue
472
- windows_read_next_byte
473
- else
474
- STDIN.read_nonblock(1) rescue nil
458
+ # CSIシーケンス(矢印キー・マウスなど)
459
+ third_char = read_next_input_byte
460
+ if third_char == '<'
461
+ # SGR拡張マウスイベント: \e[<Btn;Col;RowM/m
462
+ mouse_seq = +""
463
+ loop do
464
+ ch = read_next_input_byte
465
+ break if ch.nil?
466
+ mouse_seq << ch
467
+ break if ch == 'M' || ch == 'm'
468
+ end
469
+ if (m = mouse_seq.match(/\A(\d+);(\d+);(\d+)([Mm])\z/))
470
+ btn = m[1].to_i
471
+ col = m[2].to_i
472
+ row = m[3].to_i
473
+ press = m[4] == 'M'
474
+ handle_mouse_event(btn, col, row, press)
475
+ end
476
+ return true
475
477
  end
476
478
  input = case third_char
477
479
  when 'A' then 'k' # Up arrow
@@ -558,6 +560,95 @@ module Rufio
558
560
  end
559
561
  end
560
562
 
563
+ # ============================
564
+ # マウスイベント処理
565
+ # ============================
566
+
567
+ # SGRマウスイベントを処理する
568
+ # @param btn [Integer] SGRボタン番号(0=左, 1=中, 2=右, 64=ホイールアップ, 65=ホイールダウン)
569
+ # @param col [Integer] クリック列(1-indexed)
570
+ # @param row [Integer] クリック行(1-indexed)
571
+ # @param press [Boolean] true=押下, false=解放
572
+ def handle_mouse_event(btn, col, row, press)
573
+ case btn
574
+ when 0 # 左クリック(押下のみ処理)
575
+ handle_mouse_left_click(col, row) if press
576
+ when 64 # ホイールアップ
577
+ handle_mouse_scroll(:up, col, row)
578
+ when 65 # ホイールダウン
579
+ handle_mouse_scroll(:down, col, row)
580
+ end
581
+ end
582
+
583
+ # マウス左クリックを処理する
584
+ def handle_mouse_left_click(col, row)
585
+ left_width = left_panel_col_width
586
+
587
+ if col <= left_width
588
+ # 左パネル(ファイルリスト)クリック
589
+ handle_mouse_file_click(row)
590
+ else
591
+ # 右パネル(プレビュー)クリック — プレビューフォーカスを切り替え
592
+ if @keybind_handler.preview_focused?
593
+ @keybind_handler.unfocus_preview_pane
594
+ else
595
+ @keybind_handler.focus_preview_pane
596
+ end
597
+ end
598
+ end
599
+
600
+ # ファイルリストのクリック行からエントリを選択する(ダブルクリック対応)
601
+ def handle_mouse_file_click(row)
602
+ # コンテンツ行はターミナル行2〜(screen_height-1)
603
+ content_height = @screen_height - 2
604
+ return unless row >= 2 && row <= @screen_height - 1
605
+
606
+ current_idx = @keybind_handler.current_index
607
+ start_index = [current_idx - content_height / 2, 0].max
608
+ target_index = start_index + (row - 2)
609
+
610
+ entries = @keybind_handler.send(:get_active_entries)
611
+ return unless target_index >= 0 && target_index < entries.length
612
+
613
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
614
+ if @last_mouse_click_index == target_index &&
615
+ @last_mouse_click_time && (now - @last_mouse_click_time) < 0.5
616
+ # ダブルクリック: Enterキーと同等の動作
617
+ @keybind_handler.handle_key("\r")
618
+ @last_mouse_click_index = nil
619
+ @last_mouse_click_time = nil
620
+ else
621
+ # シングルクリック: カーソル移動
622
+ @keybind_handler.select_index(target_index)
623
+ @last_mouse_click_index = target_index
624
+ @last_mouse_click_time = now
625
+ end
626
+ end
627
+
628
+ # マウスホイールスクロールを処理する
629
+ def handle_mouse_scroll(direction, col, _row)
630
+ left_width = left_panel_col_width
631
+
632
+ if col > left_width && @keybind_handler.preview_focused?
633
+ # プレビューペインのスクロール
634
+ case direction
635
+ when :up then @keybind_handler.scroll_preview_up
636
+ when :down then @keybind_handler.scroll_preview_down
637
+ end
638
+ else
639
+ # ファイルリストのスクロール
640
+ case direction
641
+ when :up then @keybind_handler.handle_key('k')
642
+ when :down then @keybind_handler.handle_key('j')
643
+ end
644
+ end
645
+ end
646
+
647
+ # 左パネルの列幅(1-indexed境界)を返す
648
+ def left_panel_col_width
649
+ (@screen_width * @ui_renderer.left_panel_ratio).to_i
650
+ end
651
+
561
652
  # モード変更を適用
562
653
  def apply_mode_change(mode)
563
654
  case mode
@@ -25,7 +25,7 @@ module Rufio
25
25
  attr_accessor :keybind_handler, :directory_listing, :file_preview
26
26
  attr_accessor :background_executor, :test_mode
27
27
  attr_accessor :completion_lamp_message, :completion_lamp_time
28
- attr_reader :tab_mode_manager, :highlight_updated
28
+ attr_reader :tab_mode_manager, :highlight_updated, :left_panel_ratio
29
29
 
30
30
  def preview_enabled?
31
31
  @preview_enabled
data/lib/rufio/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rufio
4
- VERSION = '0.82.1'
4
+ VERSION = '0.83.1'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rufio
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.82.1
4
+ version: 0.83.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - masisz
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-03-07 00:00:00.000000000 Z
11
+ date: 2026-03-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: io-console
@@ -139,6 +139,7 @@ files:
139
139
  - docs/CHANGELOG_v0.8.0.md
140
140
  - docs/CHANGELOG_v0.80.0.md
141
141
  - docs/CHANGELOG_v0.9.0.md
142
+ - docs/architecture_concurrency_ja_v0.82.1.md
142
143
  - examples/bookmarks.yml
143
144
  - examples/config.rb
144
145
  - examples/script_paths.yml