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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +103 -7
- data/bin/rufio +17 -2
- data/docs/CHANGELOG_v0.33.0.md +2 -2
- data/docs/CHANGELOG_v0.40.0.md +416 -0
- data/lib/rufio/application.rb +2 -2
- data/lib/rufio/async_scanner_fiber.rb +154 -0
- data/lib/rufio/async_scanner_promise.rb +66 -0
- data/lib/rufio/color_helper.rb +59 -6
- data/lib/rufio/command_logger.rb +3 -0
- data/lib/rufio/command_mode_ui.rb +18 -0
- data/lib/rufio/dialog_renderer.rb +68 -0
- data/lib/rufio/keybind_handler.rb +53 -2
- data/lib/rufio/native/rufio_zig.bundle +0 -0
- data/lib/rufio/native_scanner.rb +252 -233
- data/lib/rufio/native_scanner_zig.rb +215 -82
- data/lib/rufio/parallel_scanner.rb +173 -0
- data/lib/rufio/plugins/stop.rb +32 -0
- data/lib/rufio/renderer.rb +64 -0
- data/lib/rufio/screen.rb +184 -0
- data/lib/rufio/terminal_ui.rb +557 -34
- data/lib/rufio/text_utils.rb +30 -18
- data/lib/rufio/version.rb +1 -1
- data/lib/rufio.rb +5 -1
- data/lib_zig/rufio_native/Makefile +2 -1
- data/lib_zig/rufio_native/src/main.zig +328 -117
- data/lib_zig/rufio_native/src/main.zig.sync +205 -0
- metadata +10 -9
- data/lib/rufio/native/rufio_native.bundle +0 -0
- data/lib/rufio/native_scanner_magnus.rb +0 -194
- data/lib_rust/rufio_native/.cargo/config.toml +0 -2
- data/lib_rust/rufio_native/Cargo.lock +0 -346
- data/lib_rust/rufio_native/Cargo.toml +0 -18
- data/lib_rust/rufio_native/build.rs +0 -46
- data/lib_rust/rufio_native/src/lib.rs +0 -197
|
@@ -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
|
data/lib/rufio/color_helper.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
#
|
|
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カラー
|
data/lib/rufio/command_logger.rb
CHANGED
|
@@ -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
|
-
|
|
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
|