rufio 0.82.1 → 0.83.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 95f95e73aa18690cd88f4c18a1c1b4c7d5bccf7a07ba7c507cd569acdc3985e3
4
- data.tar.gz: c7a636b0fe589fcfa516d3109186e310132bdb478a0357d0542be76239dfc6fc
3
+ metadata.gz: b75328867a09d6dfa3b4c759e242d239ab8d087540a43af19471473cc0515c4c
4
+ data.tar.gz: 798358451d71a8b74ee70ce2d362b2f24fb887e6de9b574f700c0f25a449e389
5
5
  SHA512:
6
- metadata.gz: 5bfda6ac1bdd743ca4c54a5b5e6dc1c72614900ec280a8c972ae14e7effb1cb84d9d693ee4f74115595e5a68a7d8e81ec350a7fd03506ff413257c8d9a270066
7
- data.tar.gz: 1ab9a355aaf8d5be8f8b66458935886b210fbd2bf17519b1b6793644fe6f0f2f69ccacd5cd376986d0a1dd6be7d0692200da61080172db5a1b6db2c24fd6e6c7
6
+ metadata.gz: 8072bc03c642bbc3043caca4cfa4899c72b6622ab7a74517ef7964010a9257e938005191de9a8e8aaed444749108ef1ef983b6b4dc8483aa8b4d2a6a8cb075a6
7
+ data.tar.gz: d6fb6e10e070e94120518d5a883bd241b46a957e7938b375b09e7dc3e28e73e9652aabe7b6a191e2575872764919581ac6d4b163d48fbb2d5dcd50e7740e2e2f
data/CHANGELOG.md CHANGED
@@ -5,6 +5,24 @@ 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.0] - 2026-03-14
9
+
10
+ ### Added
11
+ - **Mouse input support**: Enabled SGR extended mouse reporting (`\e[?1003h\e[?1006h`) with click and scroll handling
12
+ - **Left click (file list)**: Move cursor to the clicked row
13
+ - **Left double-click (file list)**: Re-click within 0.5s opens the file (equivalent to Enter)
14
+ - **Left click (right panel)**: Toggle preview focus on/off
15
+ - **Wheel scroll (file list)**: Scroll up/down (equivalent to j/k)
16
+ - **Wheel scroll (preview focused)**: Scroll the preview panel
17
+ - Mouse reporting is disabled on exit via `cleanup_terminal` (`\e[?1003l\e[?1006l`)
18
+ - **Architecture documentation**: Added `docs/architecture_concurrency_ja_v0.82.1.md`
19
+ - Documents rufio's concurrency model (single-threaded UI + worker threads + child processes)
20
+ - Documents memory model characteristics (screen buffer, preview cache, job history)
21
+
22
+ ### Changed
23
+ - **`read_next_input_byte` helper**: Extracted common ESC sequence reading logic to reduce duplication
24
+ - **`UIRenderer#left_panel_ratio` exposed**: Added `attr_reader` for use in mouse click panel detection
25
+
8
26
  ## [0.82.1] - 2026-03-07
9
27
 
10
28
  ### 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,6 +185,10 @@ module Rufio
185
185
  STDIN.raw!
186
186
  end
187
187
 
188
+ # SGR拡張マウスレポートを有効化(ボタン + ホイール + 任意位置クリック)
189
+ print "\e[?1003h\e[?1006h"
190
+ STDOUT.flush
191
+
188
192
  # Windows: IO.select + read_nonblockはコンソールハンドルのESCキーを
189
193
  # 検出できない場合がある。バックグラウンドスレッドでSTDINを読み取り
190
194
  # Queueに格納することで信頼性のある入力検出を実現する。
@@ -231,6 +235,15 @@ module Rufio
231
235
  end
232
236
  end
233
237
 
238
+ # エスケープシーケンスの後続バイトを読み取る(Windows/Unix共通ヘルパー)
239
+ def read_next_input_byte
240
+ if @windows_input_queue
241
+ windows_read_next_byte
242
+ else
243
+ STDIN.read_nonblock(1) rescue nil
244
+ end
245
+ end
246
+
234
247
  def cleanup_terminal
235
248
  # rawモードを解除
236
249
  if STDIN.tty?
@@ -244,6 +257,10 @@ module Rufio
244
257
  @windows_input_queue = nil
245
258
  end
246
259
 
260
+ # マウスレポートを無効化
261
+ print "\e[?1003l\e[?1006l"
262
+ STDOUT.flush
263
+
247
264
  system('tput rmcup') # normal screen
248
265
  system('tput cnorm') # cursor normal
249
266
  puts ConfigLoader.message('app.terminated')
@@ -461,17 +478,27 @@ module Rufio
461
478
 
462
479
  # 特殊キーの処理(エスケープシーケンス)(コマンドモード外のみ)
463
480
  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
481
+ next_char = read_next_input_byte
469
482
  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
483
+ # CSIシーケンス(矢印キー・マウスなど)
484
+ third_char = read_next_input_byte
485
+ if third_char == '<'
486
+ # SGR拡張マウスイベント: \e[<Btn;Col;RowM/m
487
+ mouse_seq = +""
488
+ loop do
489
+ ch = read_next_input_byte
490
+ break if ch.nil?
491
+ mouse_seq << ch
492
+ break if ch == 'M' || ch == 'm'
493
+ end
494
+ if (m = mouse_seq.match(/\A(\d+);(\d+);(\d+)([Mm])\z/))
495
+ btn = m[1].to_i
496
+ col = m[2].to_i
497
+ row = m[3].to_i
498
+ press = m[4] == 'M'
499
+ handle_mouse_event(btn, col, row, press)
500
+ end
501
+ return true
475
502
  end
476
503
  input = case third_char
477
504
  when 'A' then 'k' # Up arrow
@@ -558,6 +585,95 @@ module Rufio
558
585
  end
559
586
  end
560
587
 
588
+ # ============================
589
+ # マウスイベント処理
590
+ # ============================
591
+
592
+ # SGRマウスイベントを処理する
593
+ # @param btn [Integer] SGRボタン番号(0=左, 1=中, 2=右, 64=ホイールアップ, 65=ホイールダウン)
594
+ # @param col [Integer] クリック列(1-indexed)
595
+ # @param row [Integer] クリック行(1-indexed)
596
+ # @param press [Boolean] true=押下, false=解放
597
+ def handle_mouse_event(btn, col, row, press)
598
+ case btn
599
+ when 0 # 左クリック(押下のみ処理)
600
+ handle_mouse_left_click(col, row) if press
601
+ when 64 # ホイールアップ
602
+ handle_mouse_scroll(:up, col, row)
603
+ when 65 # ホイールダウン
604
+ handle_mouse_scroll(:down, col, row)
605
+ end
606
+ end
607
+
608
+ # マウス左クリックを処理する
609
+ def handle_mouse_left_click(col, row)
610
+ left_width = left_panel_col_width
611
+
612
+ if col <= left_width
613
+ # 左パネル(ファイルリスト)クリック
614
+ handle_mouse_file_click(row)
615
+ else
616
+ # 右パネル(プレビュー)クリック — プレビューフォーカスを切り替え
617
+ if @keybind_handler.preview_focused?
618
+ @keybind_handler.unfocus_preview_pane
619
+ else
620
+ @keybind_handler.focus_preview_pane
621
+ end
622
+ end
623
+ end
624
+
625
+ # ファイルリストのクリック行からエントリを選択する(ダブルクリック対応)
626
+ def handle_mouse_file_click(row)
627
+ # コンテンツ行はターミナル行2〜(screen_height-1)
628
+ content_height = @screen_height - 2
629
+ return unless row >= 2 && row <= @screen_height - 1
630
+
631
+ current_idx = @keybind_handler.current_index
632
+ start_index = [current_idx - content_height / 2, 0].max
633
+ target_index = start_index + (row - 2)
634
+
635
+ entries = @keybind_handler.send(:get_active_entries)
636
+ return unless target_index >= 0 && target_index < entries.length
637
+
638
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
639
+ if @last_mouse_click_index == target_index &&
640
+ @last_mouse_click_time && (now - @last_mouse_click_time) < 0.5
641
+ # ダブルクリック: Enterキーと同等の動作
642
+ @keybind_handler.handle_key("\r")
643
+ @last_mouse_click_index = nil
644
+ @last_mouse_click_time = nil
645
+ else
646
+ # シングルクリック: カーソル移動
647
+ @keybind_handler.select_index(target_index)
648
+ @last_mouse_click_index = target_index
649
+ @last_mouse_click_time = now
650
+ end
651
+ end
652
+
653
+ # マウスホイールスクロールを処理する
654
+ def handle_mouse_scroll(direction, col, _row)
655
+ left_width = left_panel_col_width
656
+
657
+ if col > left_width && @keybind_handler.preview_focused?
658
+ # プレビューペインのスクロール
659
+ case direction
660
+ when :up then @keybind_handler.scroll_preview_up
661
+ when :down then @keybind_handler.scroll_preview_down
662
+ end
663
+ else
664
+ # ファイルリストのスクロール
665
+ case direction
666
+ when :up then @keybind_handler.handle_key('k')
667
+ when :down then @keybind_handler.handle_key('j')
668
+ end
669
+ end
670
+ end
671
+
672
+ # 左パネルの列幅(1-indexed境界)を返す
673
+ def left_panel_col_width
674
+ (@screen_width * @ui_renderer.left_panel_ratio).to_i
675
+ end
676
+
561
677
  # モード変更を適用
562
678
  def apply_mode_change(mode)
563
679
  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.0'
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.0
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-14 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