rufio 0.33.0 → 0.40.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.
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require 'async'
5
+ ASYNC_GEM_AVAILABLE = true
6
+ rescue LoadError
7
+ ASYNC_GEM_AVAILABLE = false
8
+ end
9
+
10
+ module Rufio
11
+ # Fiber(Asyncライブラリ)統合用ラッパークラス
12
+ #
13
+ # Asyncライブラリと統合し、ノンブロッキングで非同期スキャンを実行します。
14
+ #
15
+ # 使用例:
16
+ # Async do
17
+ # scanner = NativeScannerRubyCore.new
18
+ # wrapper = AsyncScannerFiberWrapper.new(scanner)
19
+ # entries = wrapper.scan_async('/path')
20
+ # puts "Found #{entries.length} entries"
21
+ # end
22
+ #
23
+ class AsyncScannerFiberWrapper
24
+ def initialize(scanner)
25
+ @scanner = scanner
26
+ end
27
+
28
+ # 非同期スキャンを開始し、Fiberで完了を待つ
29
+ #
30
+ # @param path [String] スキャンするディレクトリのパス
31
+ # @param timeout [Integer, nil] タイムアウト秒数(オプション)
32
+ # @return [Array<Hash>] スキャン結果
33
+ def scan_async(path, timeout: nil)
34
+ # スキャンを開始
35
+ @scanner.scan_async(path)
36
+
37
+ # Fiberでポーリング
38
+ poll_until_complete(timeout: timeout)
39
+ end
40
+
41
+ # 高速スキャン(エントリ数制限付き)
42
+ #
43
+ # @param path [String] スキャンするディレクトリのパス
44
+ # @param max_entries [Integer] 最大エントリ数
45
+ # @param timeout [Integer, nil] タイムアウト秒数(オプション)
46
+ # @return [Array<Hash>] スキャン結果
47
+ def scan_fast_async(path, max_entries, timeout: nil)
48
+ # スキャンを開始
49
+ @scanner.scan_fast_async(path, max_entries)
50
+
51
+ # Fiberでポーリング
52
+ poll_until_complete(timeout: timeout)
53
+ end
54
+
55
+ # 進捗報告付きスキャン
56
+ #
57
+ # @param path [String] スキャンするディレクトリのパス
58
+ # @param timeout [Integer, nil] タイムアウト秒数(オプション)
59
+ # @yield [current, total] 進捗情報を受け取るブロック
60
+ # @return [Array<Hash>] スキャン結果
61
+ def scan_async_with_progress(path, timeout: nil, &block)
62
+ # スキャンを開始
63
+ @scanner.scan_async(path)
64
+
65
+ # 進捗付きでポーリング
66
+ poll_until_complete_with_progress(timeout: timeout, &block)
67
+ end
68
+
69
+ # スキャンをキャンセル
70
+ def cancel
71
+ @scanner.cancel
72
+ end
73
+
74
+ # 状態を取得
75
+ #
76
+ # @return [Symbol] 現在の状態
77
+ def get_state
78
+ @scanner.get_state
79
+ end
80
+
81
+ # 進捗を取得
82
+ #
83
+ # @return [Hash] 進捗情報 {current:, total:}
84
+ def get_progress
85
+ @scanner.get_progress
86
+ end
87
+
88
+ private
89
+
90
+ # 完了までポーリング(Fiberでスリープ)
91
+ def poll_until_complete(timeout: nil)
92
+ start_time = Time.now
93
+
94
+ loop do
95
+ state = @scanner.get_state
96
+
97
+ case state
98
+ when :done
99
+ result = @scanner.get_results
100
+ @scanner.close
101
+ return result
102
+ when :failed
103
+ @scanner.close
104
+ raise StandardError, "Scan failed"
105
+ when :cancelled
106
+ @scanner.close
107
+ raise StandardError, "Scan cancelled"
108
+ end
109
+
110
+ if timeout && (Time.now - start_time) > timeout
111
+ @scanner.close
112
+ raise StandardError, "Timeout"
113
+ end
114
+
115
+ # Fiberでスリープ(ノンブロッキング)
116
+ sleep 0.01
117
+ end
118
+ end
119
+
120
+ # 進捗報告付きでポーリング
121
+ def poll_until_complete_with_progress(timeout: nil, &block)
122
+ start_time = Time.now
123
+
124
+ loop do
125
+ state = @scanner.get_state
126
+ progress = @scanner.get_progress
127
+
128
+ # 進捗コールバック実行
129
+ yield(progress[:current], progress[:total]) if block_given?
130
+
131
+ case state
132
+ when :done
133
+ result = @scanner.get_results
134
+ @scanner.close
135
+ return result
136
+ when :failed
137
+ @scanner.close
138
+ raise StandardError, "Scan failed"
139
+ when :cancelled
140
+ @scanner.close
141
+ raise StandardError, "Scan cancelled"
142
+ end
143
+
144
+ if timeout && (Time.now - start_time) > timeout
145
+ @scanner.close
146
+ raise StandardError, "Timeout"
147
+ end
148
+
149
+ # Fiberでスリープ(ノンブロッキング)
150
+ sleep 0.01
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rufio
4
+ # Promise風インターフェースで非同期スキャンを扱うクラス
5
+ #
6
+ # 使用例:
7
+ # scanner = NativeScannerRubyCore.new
8
+ # AsyncScannerPromise.new(scanner)
9
+ # .scan_async('/path')
10
+ # .then { |entries| entries.select { |e| e[:type] == 'file' } }
11
+ # .then { |files| files.map { |f| f[:name] } }
12
+ # .wait
13
+ #
14
+ class AsyncScannerPromise
15
+ def initialize(scanner)
16
+ @scanner = scanner
17
+ @callbacks = []
18
+ end
19
+
20
+ # 非同期スキャンを開始
21
+ #
22
+ # @param path [String] スキャンするディレクトリのパス
23
+ # @return [AsyncScannerPromise] self(メソッドチェーン用)
24
+ def scan_async(path)
25
+ @scanner.scan_async(path)
26
+ self
27
+ end
28
+
29
+ # 高速スキャンを開始(エントリ数制限付き)
30
+ #
31
+ # @param path [String] スキャンするディレクトリのパス
32
+ # @param max_entries [Integer] 最大エントリ数
33
+ # @return [AsyncScannerPromise] self(メソッドチェーン用)
34
+ def scan_fast_async(path, max_entries)
35
+ @scanner.scan_fast_async(path, max_entries)
36
+ self
37
+ end
38
+
39
+ # コールバックを登録
40
+ #
41
+ # @yield [result] 前のステップの結果を受け取るブロック
42
+ # @return [AsyncScannerPromise] self(メソッドチェーン用)
43
+ def then(&block)
44
+ @callbacks << block if block_given?
45
+ self
46
+ end
47
+
48
+ # スキャン完了を待ち、コールバックを順次実行
49
+ #
50
+ # @param timeout [Integer, nil] タイムアウト秒数(オプション)
51
+ # @return [Object] 最後のコールバックの戻り値、またはスキャン結果
52
+ def wait(timeout: nil)
53
+ result = @scanner.wait(timeout: timeout)
54
+
55
+ # 登録されたコールバックを順次実行
56
+ @callbacks.each do |callback|
57
+ result = callback.call(result)
58
+ end
59
+
60
+ result
61
+ ensure
62
+ # 完了後はスキャナーを自動的にクローズ
63
+ @scanner.close
64
+ end
65
+ end
66
+ end
@@ -2,6 +2,17 @@
2
2
 
3
3
  module Rufio
4
4
  class ColorHelper
5
+ # 色変換結果のキャッシュ(毎フレームの計算を回避)
6
+ # クラスインスタンス変数として初期化
7
+ @color_to_ansi_cache = {}
8
+ @color_to_selected_ansi_cache = {}
9
+ @color_to_bg_ansi_cache = {}
10
+
11
+ # キャッシュへのアクセサメソッド
12
+ class << self
13
+ attr_accessor :color_to_ansi_cache, :color_to_selected_ansi_cache, :color_to_bg_ansi_cache
14
+ end
15
+
5
16
  # HSLからRGBへの変換
6
17
  def self.hsl_to_rgb(hue, saturation, lightness)
7
18
  h = hue.to_f / 360.0
@@ -32,9 +43,16 @@ module Rufio
32
43
  [(r * 255).round, (g * 255).round, (b * 255).round]
33
44
  end
34
45
 
35
- # 色設定をANSIエスケープコードに変換
46
+ # 色設定をANSIエスケープコードに変換(キャッシュ対応)
36
47
  def self.color_to_ansi(color_config)
37
- case color_config
48
+ # キャッシュキーを生成(Hashの場合はハッシュ値を使用)
49
+ cache_key = color_config.is_a?(Hash) ? color_config.hash : color_config
50
+
51
+ # キャッシュチェック
52
+ return @color_to_ansi_cache[cache_key] if @color_to_ansi_cache.key?(cache_key)
53
+
54
+ # キャッシュミス時のみ計算
55
+ result = case color_config
38
56
  when Hash
39
57
  if color_config[:hsl]
40
58
  # HSL形式: {hsl: [240, 100, 50]}
@@ -73,6 +91,10 @@ module Rufio
73
91
  # デフォルト(白)
74
92
  "\e[37m"
75
93
  end
94
+
95
+ # キャッシュに保存
96
+ @color_to_ansi_cache[cache_key] = result
97
+ result
76
98
  end
77
99
 
78
100
  # シンボルをANSIコードに変換
@@ -103,11 +125,22 @@ module Rufio
103
125
  symbol_to_ansi(name.to_sym)
104
126
  end
105
127
 
106
- # 背景色用のANSIコードを生成
128
+ # 背景色用のANSIコードを生成(キャッシュ対応)
107
129
  def self.color_to_bg_ansi(color_config)
130
+ # キャッシュキーを生成
131
+ cache_key = color_config.is_a?(Hash) ? color_config.hash : color_config
132
+
133
+ # キャッシュチェック
134
+ return @color_to_bg_ansi_cache[cache_key] if @color_to_bg_ansi_cache.key?(cache_key)
135
+
136
+ # キャッシュミス時のみ計算
108
137
  ansi_code = color_to_ansi(color_config)
109
138
  # 前景色(38)を背景色(48)に変換
110
- ansi_code.gsub('38;', '48;')
139
+ result = ansi_code.gsub('38;', '48;')
140
+
141
+ # キャッシュに保存
142
+ @color_to_bg_ansi_cache[cache_key] = result
143
+ result
111
144
  end
112
145
 
113
146
  # リセットコード
@@ -115,11 +148,31 @@ module Rufio
115
148
  "\e[0m"
116
149
  end
117
150
 
118
- # 選択状態(反転表示)用のANSIコードを生成
151
+ # ANSI escape codes を文字列から除去
152
+ #
153
+ # @param str [String] ANSI codes を含む文字列
154
+ # @return [String] ANSI codes を除去した文字列
155
+ def self.strip_ansi(str)
156
+ return str if str.nil?
157
+ str.gsub(/\e\[[0-9;]*m/, '')
158
+ end
159
+
160
+ # 選択状態(反転表示)用のANSIコードを生成(キャッシュ対応)
119
161
  def self.color_to_selected_ansi(color_config)
162
+ # キャッシュキーを生成
163
+ cache_key = color_config.is_a?(Hash) ? color_config.hash : color_config
164
+
165
+ # キャッシュチェック
166
+ return @color_to_selected_ansi_cache[cache_key] if @color_to_selected_ansi_cache.key?(cache_key)
167
+
168
+ # キャッシュミス時のみ計算
120
169
  color_code = color_to_ansi(color_config)
121
170
  # 反転表示を追加
122
- color_code.gsub("\e[", "\e[7;").gsub("m", ";7m")
171
+ result = color_code.gsub("\e[", "\e[7;").gsub("m", ";7m")
172
+
173
+ # キャッシュに保存
174
+ @color_to_selected_ansi_cache[cache_key] = result
175
+ result
123
176
  end
124
177
 
125
178
  # プリセットHSLカラー
@@ -27,6 +27,9 @@ module Rufio
27
27
 
28
28
  content = format_log_content(command, output, timestamp, success, error)
29
29
 
30
+ # ディレクトリが存在しない場合は作成(バックグラウンドスレッドでの実行時の競合を防ぐ)
31
+ FileUtils.mkdir_p(@log_dir) unless Dir.exist?(@log_dir)
32
+
30
33
  File.write(filepath, content)
31
34
  end
32
35
 
@@ -8,6 +8,8 @@ module Rufio
8
8
  def initialize(command_mode, dialog_renderer)
9
9
  @command_mode = command_mode
10
10
  @dialog_renderer = dialog_renderer
11
+ # 最後に表示したウィンドウの位置とサイズを保存
12
+ @last_window = nil
11
13
  end
12
14
 
13
15
  # 入力文字列に対する補完候補を取得
@@ -68,6 +70,9 @@ module Rufio
68
70
  # 中央位置を計算
69
71
  x, y = @dialog_renderer.calculate_center(width, height)
70
72
 
73
+ # ウィンドウの位置とサイズを保存
74
+ @last_window = { x: x, y: y, width: width, height: height }
75
+
71
76
  # フローティングウィンドウを描画
72
77
  @dialog_renderer.draw_floating_window(x, y, width, height, title, content_lines, {
73
78
  border_color: border_color,
@@ -136,6 +141,19 @@ module Rufio
136
141
  @dialog_renderer.clear_area(x, y, width, height)
137
142
  end
138
143
 
144
+ # コマンド入力プロンプトをクリア
145
+ def clear_prompt
146
+ return unless @last_window
147
+
148
+ @dialog_renderer.clear_area(
149
+ @last_window[:x],
150
+ @last_window[:y],
151
+ @last_window[:width],
152
+ @last_window[:height]
153
+ )
154
+ @last_window = nil
155
+ end
156
+
139
157
  private
140
158
 
141
159
  # 文字列配列の共通プレフィックスを見つける
@@ -7,6 +7,74 @@ module Rufio
7
7
  class DialogRenderer
8
8
  include TextUtils
9
9
 
10
+ # Phase 4: Screenバッファにフローティングウィンドウを描画
11
+ # @param screen [Screen] Screen buffer to draw to
12
+ # @param x [Integer] X position (column)
13
+ # @param y [Integer] Y position (row)
14
+ # @param width [Integer] Window width
15
+ # @param height [Integer] Window height
16
+ # @param title [String, nil] Window title (optional)
17
+ # @param content_lines [Array<String>] Content lines to display
18
+ # @param options [Hash] Customization options
19
+ # @option options [String] :border_color Border color ANSI code
20
+ # @option options [String] :title_color Title color ANSI code
21
+ # @option options [String] :content_color Content color ANSI code
22
+ def draw_floating_window_to_buffer(screen, x, y, width, height, title, content_lines, options = {})
23
+ # Default options
24
+ border_color = options[:border_color] || "\e[37m" # White
25
+ title_color = options[:title_color] || "\e[1;33m" # Bold yellow
26
+ content_color = options[:content_color] || "\e[37m" # White
27
+
28
+ # Draw top border
29
+ screen.put_string(x, y, "┌#{'─' * (width - 2)}┐", fg: border_color)
30
+
31
+ # Draw title line if title exists
32
+ if title
33
+ title_width = TextUtils.display_width(title)
34
+ title_padding = (width - 2 - title_width) / 2
35
+ padded_title = ' ' * title_padding + title
36
+ title_line = TextUtils.pad_string_to_width(padded_title, width - 2)
37
+
38
+ screen.put(x, y + 1, '│', fg: border_color)
39
+ screen.put_string(x + 1, y + 1, title_line, fg: title_color)
40
+ screen.put(x + width - 1, y + 1, '│', fg: border_color)
41
+
42
+ # Draw title separator
43
+ screen.put_string(x, y + 2, "├#{'─' * (width - 2)}┤", fg: border_color)
44
+ content_start_y = y + 3
45
+ else
46
+ content_start_y = y + 1
47
+ end
48
+
49
+ # Draw content lines
50
+ content_height = title ? height - 4 : height - 2
51
+ content_lines.each_with_index do |line, index|
52
+ break if index >= content_height
53
+
54
+ line_y = content_start_y + index
55
+ line_content = TextUtils.pad_string_to_width(line, width - 2)
56
+
57
+ screen.put(x, line_y, '│', fg: border_color)
58
+ screen.put_string(x + 1, line_y, line_content, fg: content_color)
59
+ screen.put(x + width - 1, line_y, '│', fg: border_color)
60
+ end
61
+
62
+ # Fill remaining lines with empty space
63
+ remaining_lines = content_height - content_lines.length
64
+ remaining_lines.times do |i|
65
+ line_y = content_start_y + content_lines.length + i
66
+ empty_line = ' ' * (width - 2)
67
+
68
+ screen.put(x, line_y, '│', fg: border_color)
69
+ screen.put_string(x + 1, line_y, empty_line)
70
+ screen.put(x + width - 1, line_y, '│', fg: border_color)
71
+ end
72
+
73
+ # Draw bottom border
74
+ bottom_y = y + height - 1
75
+ screen.put_string(x, bottom_y, "└#{'─' * (width - 2)}┘", fg: border_color)
76
+ end
77
+
10
78
  # Draw a floating window with title, content, and customizable colors
11
79
  # @param x [Integer] X position (column)
12
80
  # @param y [Integer] Y position (row)
@@ -545,7 +545,7 @@ module Rufio
545
545
  end
546
546
 
547
547
  def exit_request
548
- true # request exit
548
+ show_exit_confirmation
549
549
  end
550
550
 
551
551
  def fzf_search
@@ -1117,6 +1117,53 @@ module Rufio
1117
1117
  end
1118
1118
  end
1119
1119
 
1120
+ def show_exit_confirmation
1121
+ # コンテンツの準備
1122
+ title = 'Exit Confirmation'
1123
+
1124
+ content_lines = [
1125
+ '',
1126
+ 'Are you sure you want to exit?',
1127
+ '',
1128
+ ' [Y]es - Exit',
1129
+ ' [N]o - Cancel',
1130
+ ''
1131
+ ]
1132
+
1133
+ # ダイアログのサイズ設定
1134
+ dialog_width = CONFIRMATION_DIALOG_WIDTH
1135
+ dialog_height = DIALOG_BORDER_HEIGHT + content_lines.length
1136
+
1137
+ # ダイアログの位置を中央に設定
1138
+ x, y = @dialog_renderer.calculate_center(dialog_width, dialog_height)
1139
+
1140
+ # ダイアログの描画(終了は黄色で表示)
1141
+ @dialog_renderer.draw_floating_window(x, y, dialog_width, dialog_height, title, content_lines, {
1142
+ border_color: "\e[33m", # 黄色(注意)
1143
+ title_color: "\e[1;33m", # 太字黄色
1144
+ content_color: "\e[37m" # 白色
1145
+ })
1146
+
1147
+ # キー入力待機
1148
+ loop do
1149
+ input = STDIN.getch.downcase
1150
+
1151
+ case input
1152
+ when 'y'
1153
+ # ダイアログをクリア
1154
+ @dialog_renderer.clear_area(x, y, dialog_width, dialog_height)
1155
+ @terminal_ui&.refresh_display # 画面を再描画
1156
+ return true
1157
+ when 'n', "\e", "\x03" # n, ESC, Ctrl+C
1158
+ # ダイアログをクリア
1159
+ @dialog_renderer.clear_area(x, y, dialog_width, dialog_height)
1160
+ @terminal_ui&.refresh_display # 画面を再描画
1161
+ return false
1162
+ end
1163
+ # 無効なキー入力の場合は再度ループ
1164
+ end
1165
+ end
1166
+
1120
1167
  # パスを指定した長さに短縮
1121
1168
  def shorten_path(path, max_length)
1122
1169
  return path if path.length <= max_length
@@ -1455,7 +1502,7 @@ module Rufio
1455
1502
 
1456
1503
  # プロジェクトモード中のキー処理
1457
1504
  def handle_project_mode_key(key)
1458
- case key
1505
+ result = case key
1459
1506
  when "\e" # ESC - ログモードならプロジェクトモードに戻る、そうでなければ終了
1460
1507
  if @in_log_mode
1461
1508
  exit_log_mode
@@ -1507,6 +1554,10 @@ module Rufio
1507
1554
  else
1508
1555
  false
1509
1556
  end
1557
+
1558
+ # キー処理後、プロジェクトモードの再描画をトリガー
1559
+ @terminal_ui&.trigger_project_mode_redraw if result && @in_project_mode
1560
+ result
1510
1561
  end
1511
1562
 
1512
1563
  # プロジェクトモード用のエントリ数取得
Binary file