rufio 0.40.1 → 0.50.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 +25 -0
- data/README.md +64 -158
- data/bin/rufio +11 -4
- data/docs/CHANGELOG_v0.41.0.md +533 -0
- data/docs/CHANGELOG_v0.50.0.md +128 -0
- data/lib/rufio/background_command_executor.rb +64 -2
- data/lib/rufio/builtin_commands.rb +34 -0
- data/lib/rufio/command_mode.rb +67 -35
- data/lib/rufio/dsl_command.rb +120 -0
- data/lib/rufio/dsl_command_loader.rb +177 -0
- data/lib/rufio/file_preview.rb +22 -6
- data/lib/rufio/interpreter_resolver.rb +79 -0
- data/lib/rufio/renderer.rb +15 -2
- data/lib/rufio/script_executor.rb +253 -0
- data/lib/rufio/terminal_ui.rb +145 -47
- data/lib/rufio/text_utils.rb +42 -19
- data/lib/rufio/version.rb +1 -1
- data/lib/rufio.rb +7 -7
- metadata +9 -8
- data/lib/rufio/plugin.rb +0 -89
- data/lib/rufio/plugin_config.rb +0 -59
- data/lib/rufio/plugin_manager.rb +0 -84
- data/lib/rufio/plugins/file_operations.rb +0 -44
- data/lib/rufio/plugins/hello.rb +0 -30
- data/lib/rufio/plugins/stop.rb +0 -32
data/lib/rufio/renderer.rb
CHANGED
|
@@ -22,20 +22,33 @@ module Rufio
|
|
|
22
22
|
# Render the screen with differential updates
|
|
23
23
|
#
|
|
24
24
|
# @param screen [Screen] The back buffer to render
|
|
25
|
+
# @return [Boolean] true if rendering was performed, false if skipped
|
|
25
26
|
def render(screen)
|
|
27
|
+
# CPU最適化: Dirty rowsが空の場合は完全にスキップ
|
|
28
|
+
dirty = screen.dirty_rows
|
|
29
|
+
if dirty.empty?
|
|
30
|
+
return false
|
|
31
|
+
end
|
|
32
|
+
|
|
26
33
|
# Phase1: Only process dirty rows (rows that have changed)
|
|
27
|
-
|
|
34
|
+
rendered_count = 0
|
|
35
|
+
dirty.each do |y|
|
|
28
36
|
line = screen.row(y)
|
|
29
37
|
next if line == @front[y] # Skip if content is actually the same
|
|
30
38
|
|
|
31
39
|
# Move cursor to line y (1-indexed) and output the line
|
|
32
40
|
@output.print "\e[#{y + 1};1H#{line}"
|
|
33
41
|
@front[y] = line
|
|
42
|
+
rendered_count += 1
|
|
34
43
|
end
|
|
35
44
|
|
|
36
45
|
# Phase1: Clear dirty tracking after rendering
|
|
37
46
|
screen.clear_dirty
|
|
38
|
-
|
|
47
|
+
|
|
48
|
+
# Only flush if we actually rendered something
|
|
49
|
+
@output.flush if rendered_count > 0
|
|
50
|
+
|
|
51
|
+
true
|
|
39
52
|
end
|
|
40
53
|
|
|
41
54
|
# Resize the front buffer
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "timeout"
|
|
5
|
+
|
|
6
|
+
module Rufio
|
|
7
|
+
# スクリプトを安全に実行するクラス
|
|
8
|
+
class ScriptExecutor
|
|
9
|
+
class << self
|
|
10
|
+
# スクリプトを実行する
|
|
11
|
+
# @param interpreter [String] インタープリタ(ruby, python3, bashなど)
|
|
12
|
+
# @param script_path [String] スクリプトのパス
|
|
13
|
+
# @param args [Array<String>] スクリプトへの引数
|
|
14
|
+
# @param timeout [Numeric, nil] タイムアウト秒数(nilの場合は無制限)
|
|
15
|
+
# @param chdir [String, nil] 作業ディレクトリ
|
|
16
|
+
# @param env [Hash, nil] 環境変数
|
|
17
|
+
# @return [Hash] 実行結果
|
|
18
|
+
def execute(interpreter, script_path, args = [], timeout: nil, chdir: nil, env: nil)
|
|
19
|
+
# 配列ベースのコマンドを構築(シェルインジェクション防止)
|
|
20
|
+
command = [interpreter, script_path, *args]
|
|
21
|
+
|
|
22
|
+
# オプションを構築
|
|
23
|
+
options = {}
|
|
24
|
+
options[:chdir] = chdir if chdir
|
|
25
|
+
# 環境変数をマージ(既存の環境変数を保持)
|
|
26
|
+
spawn_env = env || {}
|
|
27
|
+
|
|
28
|
+
execute_with_options(command, spawn_env, options, timeout)
|
|
29
|
+
rescue StandardError => e
|
|
30
|
+
build_error_result(e)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# DslCommandを実行する(タイプ別に分岐)
|
|
34
|
+
# @param dsl_command [DslCommand] 実行するDSLコマンド
|
|
35
|
+
# @param args [Array<String>] 追加の引数
|
|
36
|
+
# @param timeout [Numeric, nil] タイムアウト秒数
|
|
37
|
+
# @param chdir [String, nil] 作業ディレクトリ
|
|
38
|
+
# @param env [Hash, nil] 環境変数
|
|
39
|
+
# @return [Hash] 実行結果
|
|
40
|
+
def execute_command(dsl_command, args = [], timeout: nil, chdir: nil, env: nil)
|
|
41
|
+
case dsl_command.command_type
|
|
42
|
+
when :ruby
|
|
43
|
+
execute_ruby(dsl_command)
|
|
44
|
+
when :shell
|
|
45
|
+
execute_shell(dsl_command, timeout: timeout, chdir: chdir, env: env)
|
|
46
|
+
else
|
|
47
|
+
exec_args = dsl_command.to_execution_args
|
|
48
|
+
execute(exec_args[0], exec_args[1], args, timeout: timeout, chdir: chdir, env: env)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# inline Rubyコマンドを実行する
|
|
53
|
+
# @param dsl_command [DslCommand] 実行するDSLコマンド
|
|
54
|
+
# @return [Hash] 実行結果
|
|
55
|
+
def execute_ruby(dsl_command)
|
|
56
|
+
result = dsl_command.ruby_block.call
|
|
57
|
+
{
|
|
58
|
+
success: true,
|
|
59
|
+
exit_code: 0,
|
|
60
|
+
stdout: result.to_s,
|
|
61
|
+
stderr: "",
|
|
62
|
+
timeout: false
|
|
63
|
+
}
|
|
64
|
+
rescue StandardError => e
|
|
65
|
+
{
|
|
66
|
+
success: false,
|
|
67
|
+
exit_code: 1,
|
|
68
|
+
stdout: "",
|
|
69
|
+
stderr: "",
|
|
70
|
+
error: e.message,
|
|
71
|
+
timeout: false
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# inline シェルコマンドを実行する
|
|
76
|
+
# @param dsl_command [DslCommand] 実行するDSLコマンド
|
|
77
|
+
# @param timeout [Numeric, nil] タイムアウト秒数
|
|
78
|
+
# @param chdir [String, nil] 作業ディレクトリ
|
|
79
|
+
# @param env [Hash, nil] 環境変数
|
|
80
|
+
# @return [Hash] 実行結果
|
|
81
|
+
def execute_shell(dsl_command, timeout: nil, chdir: nil, env: nil)
|
|
82
|
+
options = {}
|
|
83
|
+
options[:chdir] = chdir if chdir
|
|
84
|
+
spawn_env = env || {}
|
|
85
|
+
|
|
86
|
+
if timeout
|
|
87
|
+
execute_shell_with_timeout(dsl_command.shell_command, spawn_env, options, timeout)
|
|
88
|
+
else
|
|
89
|
+
execute_shell_without_timeout(dsl_command.shell_command, spawn_env, options)
|
|
90
|
+
end
|
|
91
|
+
rescue StandardError => e
|
|
92
|
+
build_error_result(e)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
# コマンドを実行し、結果を返す
|
|
98
|
+
# @param command [Array<String>] 実行するコマンド
|
|
99
|
+
# @param env [Hash] 環境変数
|
|
100
|
+
# @param options [Hash] Open3オプション
|
|
101
|
+
# @param timeout_sec [Numeric, nil] タイムアウト秒数
|
|
102
|
+
# @return [Hash] 実行結果
|
|
103
|
+
def execute_with_options(command, env, options, timeout_sec)
|
|
104
|
+
if timeout_sec
|
|
105
|
+
execute_with_timeout(command, env, options, timeout_sec)
|
|
106
|
+
else
|
|
107
|
+
execute_without_timeout(command, env, options)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# タイムアウト付きで実行
|
|
112
|
+
def execute_with_timeout(command, env, options, timeout_sec)
|
|
113
|
+
stdout = ""
|
|
114
|
+
stderr = ""
|
|
115
|
+
status = nil
|
|
116
|
+
timed_out = false
|
|
117
|
+
pid = nil
|
|
118
|
+
|
|
119
|
+
begin
|
|
120
|
+
Timeout.timeout(timeout_sec) do
|
|
121
|
+
stdin, stdout_io, stderr_io, wait_thread = Open3.popen3(env, *command, **options)
|
|
122
|
+
pid = wait_thread.pid
|
|
123
|
+
stdin.close
|
|
124
|
+
stdout = stdout_io.read
|
|
125
|
+
stderr = stderr_io.read
|
|
126
|
+
stdout_io.close
|
|
127
|
+
stderr_io.close
|
|
128
|
+
status = wait_thread.value
|
|
129
|
+
end
|
|
130
|
+
rescue Timeout::Error
|
|
131
|
+
timed_out = true
|
|
132
|
+
# プロセスを終了
|
|
133
|
+
if pid
|
|
134
|
+
begin
|
|
135
|
+
Process.kill("TERM", pid)
|
|
136
|
+
sleep 0.1
|
|
137
|
+
Process.kill("KILL", pid)
|
|
138
|
+
rescue Errno::ESRCH, Errno::EPERM
|
|
139
|
+
# プロセスが既に終了している、または権限がない
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
if timed_out
|
|
145
|
+
{
|
|
146
|
+
success: false,
|
|
147
|
+
exit_code: nil,
|
|
148
|
+
stdout: stdout,
|
|
149
|
+
stderr: stderr,
|
|
150
|
+
timeout: true
|
|
151
|
+
}
|
|
152
|
+
else
|
|
153
|
+
{
|
|
154
|
+
success: status&.success? || false,
|
|
155
|
+
exit_code: status&.exitstatus || 1,
|
|
156
|
+
stdout: stdout,
|
|
157
|
+
stderr: stderr,
|
|
158
|
+
timeout: false
|
|
159
|
+
}
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# タイムアウトなしで実行
|
|
164
|
+
def execute_without_timeout(command, env, options)
|
|
165
|
+
stdout, stderr, status = Open3.capture3(env, *command, **options)
|
|
166
|
+
|
|
167
|
+
{
|
|
168
|
+
success: status.success?,
|
|
169
|
+
exit_code: status.exitstatus,
|
|
170
|
+
stdout: stdout,
|
|
171
|
+
stderr: stderr,
|
|
172
|
+
timeout: false
|
|
173
|
+
}
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# シェルコマンドをタイムアウト付きで実行
|
|
177
|
+
def execute_shell_with_timeout(shell_command, env, options, timeout_sec)
|
|
178
|
+
stdout = ""
|
|
179
|
+
stderr = ""
|
|
180
|
+
status = nil
|
|
181
|
+
timed_out = false
|
|
182
|
+
pid = nil
|
|
183
|
+
|
|
184
|
+
begin
|
|
185
|
+
Timeout.timeout(timeout_sec) do
|
|
186
|
+
stdin, stdout_io, stderr_io, wait_thread = Open3.popen3(env, shell_command, **options)
|
|
187
|
+
pid = wait_thread.pid
|
|
188
|
+
stdin.close
|
|
189
|
+
stdout = stdout_io.read
|
|
190
|
+
stderr = stderr_io.read
|
|
191
|
+
stdout_io.close
|
|
192
|
+
stderr_io.close
|
|
193
|
+
status = wait_thread.value
|
|
194
|
+
end
|
|
195
|
+
rescue Timeout::Error
|
|
196
|
+
timed_out = true
|
|
197
|
+
if pid
|
|
198
|
+
begin
|
|
199
|
+
Process.kill("TERM", pid)
|
|
200
|
+
sleep 0.1
|
|
201
|
+
Process.kill("KILL", pid)
|
|
202
|
+
rescue Errno::ESRCH, Errno::EPERM
|
|
203
|
+
# プロセスが既に終了している、または権限がない
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
if timed_out
|
|
209
|
+
{
|
|
210
|
+
success: false,
|
|
211
|
+
exit_code: nil,
|
|
212
|
+
stdout: stdout,
|
|
213
|
+
stderr: stderr,
|
|
214
|
+
timeout: true
|
|
215
|
+
}
|
|
216
|
+
else
|
|
217
|
+
{
|
|
218
|
+
success: status&.success? || false,
|
|
219
|
+
exit_code: status&.exitstatus || 1,
|
|
220
|
+
stdout: stdout,
|
|
221
|
+
stderr: stderr,
|
|
222
|
+
timeout: false
|
|
223
|
+
}
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# シェルコマンドをタイムアウトなしで実行
|
|
228
|
+
def execute_shell_without_timeout(shell_command, env, options)
|
|
229
|
+
stdout, stderr, status = Open3.capture3(env, shell_command, **options)
|
|
230
|
+
|
|
231
|
+
{
|
|
232
|
+
success: status.success?,
|
|
233
|
+
exit_code: status.exitstatus,
|
|
234
|
+
stdout: stdout,
|
|
235
|
+
stderr: stderr,
|
|
236
|
+
timeout: false
|
|
237
|
+
}
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# エラー結果を構築
|
|
241
|
+
def build_error_result(error)
|
|
242
|
+
{
|
|
243
|
+
success: false,
|
|
244
|
+
exit_code: 1,
|
|
245
|
+
stdout: "",
|
|
246
|
+
stderr: "",
|
|
247
|
+
error: error.message,
|
|
248
|
+
timeout: false
|
|
249
|
+
}
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
data/lib/rufio/terminal_ui.rb
CHANGED
|
@@ -71,6 +71,10 @@ module Rufio
|
|
|
71
71
|
@cached_bookmarks = nil
|
|
72
72
|
@cached_bookmark_time = nil
|
|
73
73
|
@bookmark_cache_ttl = 1.0 # 1秒間キャッシュ
|
|
74
|
+
|
|
75
|
+
# Command execution lamp (footer indicator)
|
|
76
|
+
@completion_lamp_message = nil
|
|
77
|
+
@completion_lamp_time = nil
|
|
74
78
|
end
|
|
75
79
|
|
|
76
80
|
def start(directory_listing, keybind_handler, file_preview, background_executor = nil)
|
|
@@ -159,75 +163,143 @@ module Rufio
|
|
|
159
163
|
puts ConfigLoader.message('app.terminated')
|
|
160
164
|
end
|
|
161
165
|
|
|
162
|
-
# ゲームループパターンのmain_loop
|
|
166
|
+
# ゲームループパターンのmain_loop(CPU最適化版:フレームスキップ対応)
|
|
163
167
|
# UPDATE → DRAW → RENDER → SLEEP のサイクル
|
|
168
|
+
# 変更がない場合は描画をスキップしてCPU使用率を削減
|
|
164
169
|
def main_loop
|
|
165
|
-
|
|
166
|
-
|
|
170
|
+
# CPU最適化: 固定FPSをやめて、イベントドリブンに変更
|
|
171
|
+
# 最小スリープ時間(入力チェック間隔)
|
|
172
|
+
min_sleep_interval = 0.0333 # 30FPS(約33.33ms/フレーム)
|
|
173
|
+
check_interval = 0.1 # バックグラウンドタスクのチェック間隔
|
|
167
174
|
|
|
168
175
|
# Phase 3: Screen/Rendererを初期化
|
|
169
176
|
@screen = Screen.new(@screen_width, @screen_height)
|
|
170
177
|
@renderer = Renderer.new(@screen_width, @screen_height)
|
|
171
178
|
|
|
179
|
+
# 初回描画
|
|
180
|
+
@screen.clear
|
|
181
|
+
draw_screen_to_buffer(@screen, nil, nil)
|
|
182
|
+
@renderer.render(@screen)
|
|
183
|
+
|
|
172
184
|
last_notification_check = Time.now
|
|
185
|
+
last_lamp_check = Time.now
|
|
173
186
|
notification_message = nil
|
|
174
187
|
notification_time = nil
|
|
188
|
+
previous_notification = nil
|
|
189
|
+
previous_lamp_message = @completion_lamp_message
|
|
175
190
|
|
|
176
191
|
# FPS計測用
|
|
177
192
|
frame_times = []
|
|
178
193
|
last_frame_time = Time.now
|
|
179
194
|
current_fps = 0.0
|
|
195
|
+
last_fps_update = Time.now
|
|
196
|
+
|
|
197
|
+
# 再描画フラグ
|
|
198
|
+
needs_redraw = false
|
|
180
199
|
|
|
181
200
|
while @running
|
|
182
201
|
start = Time.now
|
|
183
202
|
|
|
203
|
+
# FPS計算(毎フレームで記録)- ループの最初で計測してsleep時間を含める
|
|
204
|
+
if @test_mode
|
|
205
|
+
frame_time = start - last_frame_time
|
|
206
|
+
last_frame_time = start
|
|
207
|
+
frame_times << frame_time
|
|
208
|
+
frame_times.shift if frame_times.size > 60 # 直近60フレームで平均
|
|
209
|
+
|
|
210
|
+
# FPS表示の更新は1秒ごと
|
|
211
|
+
if (start - last_fps_update) > 1.0
|
|
212
|
+
avg_frame_time = frame_times.sum / frame_times.size
|
|
213
|
+
current_fps = 1.0 / avg_frame_time if avg_frame_time > 0
|
|
214
|
+
last_fps_update = start
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# test_modeでは毎フレーム描画してFPS計測の精度を上げる
|
|
218
|
+
needs_redraw = true
|
|
219
|
+
end
|
|
220
|
+
|
|
184
221
|
# UPDATE phase - ノンブロッキング入力処理
|
|
185
|
-
|
|
222
|
+
# 入力があった場合は再描画が必要
|
|
223
|
+
had_input = handle_input_nonblocking
|
|
224
|
+
needs_redraw = true if had_input
|
|
186
225
|
|
|
187
|
-
# バックグラウンドコマンドの完了チェック(0.
|
|
188
|
-
if @background_executor && (
|
|
226
|
+
# バックグラウンドコマンドの完了チェック(0.1秒ごと)
|
|
227
|
+
if @background_executor && (start - last_notification_check) > check_interval
|
|
189
228
|
if !@background_executor.running? && @background_executor.get_completion_message
|
|
190
|
-
|
|
191
|
-
|
|
229
|
+
completion_msg = @background_executor.get_completion_message
|
|
230
|
+
# 通知メッセージとして表示
|
|
231
|
+
notification_message = completion_msg
|
|
232
|
+
notification_time = start
|
|
233
|
+
# フッターのランプ表示用にも設定
|
|
234
|
+
@completion_lamp_message = completion_msg
|
|
235
|
+
@completion_lamp_time = start
|
|
192
236
|
@background_executor.instance_variable_set(:@completion_message, nil) # メッセージをクリア
|
|
237
|
+
needs_redraw = true
|
|
193
238
|
end
|
|
194
|
-
last_notification_check =
|
|
239
|
+
last_notification_check = start
|
|
195
240
|
end
|
|
196
241
|
|
|
197
|
-
#
|
|
198
|
-
if @
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
last_frame_time = Time.now
|
|
242
|
+
# バックグラウンドコマンドの実行状態が変わった場合も再描画
|
|
243
|
+
if @background_executor
|
|
244
|
+
current_running = @background_executor.running?
|
|
245
|
+
if @last_bg_running != current_running
|
|
246
|
+
@last_bg_running = current_running
|
|
247
|
+
needs_redraw = true
|
|
248
|
+
end
|
|
205
249
|
end
|
|
206
250
|
|
|
207
|
-
#
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
251
|
+
# 完了ランプの表示状態をチェック(0.5秒ごと)
|
|
252
|
+
if (start - last_lamp_check) > 0.5
|
|
253
|
+
current_lamp = @completion_lamp_message
|
|
254
|
+
if current_lamp != previous_lamp_message
|
|
255
|
+
previous_lamp_message = current_lamp
|
|
256
|
+
needs_redraw = true
|
|
257
|
+
end
|
|
258
|
+
# 完了ランプのタイムアウトチェック
|
|
259
|
+
if @completion_lamp_message && @completion_lamp_time && (start - @completion_lamp_time) >= 3.0
|
|
260
|
+
@completion_lamp_message = nil
|
|
261
|
+
needs_redraw = true
|
|
262
|
+
end
|
|
263
|
+
last_lamp_check = start
|
|
214
264
|
end
|
|
215
265
|
|
|
216
|
-
#
|
|
217
|
-
|
|
266
|
+
# 通知メッセージの変化をチェック
|
|
267
|
+
current_notification = notification_message && (start - notification_time) < 3.0 ? notification_message : nil
|
|
268
|
+
if current_notification != previous_notification
|
|
269
|
+
previous_notification = current_notification
|
|
270
|
+
notification_message = nil if current_notification.nil?
|
|
271
|
+
needs_redraw = true
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# DRAW & RENDER phase - 変更があった場合のみ描画
|
|
275
|
+
if needs_redraw
|
|
276
|
+
# Screenバッファに描画(clearは呼ばない。必要な部分だけ更新)
|
|
277
|
+
if notification_message && (start - notification_time) < 3.0
|
|
278
|
+
draw_screen_to_buffer(@screen, notification_message, current_fps)
|
|
279
|
+
else
|
|
280
|
+
draw_screen_to_buffer(@screen, nil, current_fps)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# 差分レンダリング(dirty rowsのみ)
|
|
284
|
+
@renderer.render(@screen)
|
|
285
|
+
|
|
286
|
+
# 描画後にカーソルを画面外に移動
|
|
287
|
+
if !@command_mode_active
|
|
288
|
+
print "\e[#{@screen_height};#{@screen_width}H"
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
needs_redraw = false
|
|
292
|
+
end
|
|
218
293
|
|
|
219
294
|
# コマンドモードがアクティブな場合はフローティングウィンドウを表示
|
|
220
295
|
# Phase 4: 暫定的に直接描画(Screenバッファ外)
|
|
221
296
|
if @command_mode_active
|
|
222
297
|
@command_mode_ui.show_input_prompt(@command_input)
|
|
223
|
-
else
|
|
224
|
-
# カーソルを画面外に移動
|
|
225
|
-
print "\e[#{@screen_height};#{@screen_width}H"
|
|
226
298
|
end
|
|
227
299
|
|
|
228
|
-
# SLEEP phase -
|
|
300
|
+
# SLEEP phase - CPU使用率削減のため適切にスリープ
|
|
229
301
|
elapsed = Time.now - start
|
|
230
|
-
sleep_time = [
|
|
302
|
+
sleep_time = [min_sleep_interval - elapsed, 0].max
|
|
231
303
|
sleep sleep_time if sleep_time > 0
|
|
232
304
|
end
|
|
233
305
|
end
|
|
@@ -829,13 +901,36 @@ module Rufio
|
|
|
829
901
|
end
|
|
830
902
|
bookmark_text = bookmark_parts.join(" ")
|
|
831
903
|
|
|
832
|
-
# 右側の情報: FPS(test modeの時のみ)| ?:help
|
|
904
|
+
# 右側の情報: コマンド実行ランプ | FPS(test modeの時のみ)| ?:help
|
|
905
|
+
right_parts = []
|
|
906
|
+
|
|
907
|
+
# バックグラウンドコマンドの実行状態をランプで表示
|
|
908
|
+
if @background_executor
|
|
909
|
+
if @background_executor.running?
|
|
910
|
+
# 実行中ランプ(緑色の回転矢印)
|
|
911
|
+
command_name = @background_executor.current_command || "処理中"
|
|
912
|
+
right_parts << "\e[32m🔄\e[0m #{command_name}"
|
|
913
|
+
elsif @completion_lamp_message && @completion_lamp_time
|
|
914
|
+
# 完了ランプ(3秒間表示)
|
|
915
|
+
if (Time.now - @completion_lamp_time) < 3.0
|
|
916
|
+
right_parts << @completion_lamp_message
|
|
917
|
+
else
|
|
918
|
+
@completion_lamp_message = nil
|
|
919
|
+
@completion_lamp_time = nil
|
|
920
|
+
end
|
|
921
|
+
end
|
|
922
|
+
end
|
|
923
|
+
|
|
924
|
+
# FPS表示(test modeの時のみ)
|
|
833
925
|
if @test_mode && fps
|
|
834
|
-
|
|
835
|
-
else
|
|
836
|
-
right_info = "?:help"
|
|
926
|
+
right_parts << "#{fps.round(1)} FPS"
|
|
837
927
|
end
|
|
838
928
|
|
|
929
|
+
# ヘルプ表示
|
|
930
|
+
right_parts << "?:help"
|
|
931
|
+
|
|
932
|
+
right_info = right_parts.join(" | ")
|
|
933
|
+
|
|
839
934
|
# ブックマーク一覧を利用可能な幅に収める
|
|
840
935
|
available_width = @screen_width - right_info.length - 3
|
|
841
936
|
if bookmark_text.length > available_width && available_width > 3
|
|
@@ -918,26 +1013,26 @@ module Rufio
|
|
|
918
1013
|
# ノンブロッキング入力処理(ゲームループ用)
|
|
919
1014
|
# IO.selectでタイムアウト付きで入力をチェック
|
|
920
1015
|
def handle_input_nonblocking
|
|
921
|
-
#
|
|
922
|
-
ready = IO.select([STDIN], nil, nil, 0
|
|
923
|
-
return unless ready
|
|
1016
|
+
# 0msタイムアウトで即座にチェック(30FPS = 33.33ms/frame)
|
|
1017
|
+
ready = IO.select([STDIN], nil, nil, 0)
|
|
1018
|
+
return false unless ready
|
|
924
1019
|
|
|
925
1020
|
begin
|
|
926
1021
|
# read_nonblockを使ってノンブロッキングで1文字読み取る
|
|
927
1022
|
input = STDIN.read_nonblock(1)
|
|
928
1023
|
rescue IO::WaitReadable, IO::EAGAINWaitReadable
|
|
929
1024
|
# 入力が利用できない
|
|
930
|
-
return
|
|
1025
|
+
return false
|
|
931
1026
|
rescue Errno::ENOTTY, Errno::ENODEV
|
|
932
1027
|
# ターミナルでない環境
|
|
933
|
-
return
|
|
1028
|
+
return false
|
|
934
1029
|
end
|
|
935
1030
|
|
|
936
1031
|
# コマンドモードがアクティブな場合は、エスケープシーケンス処理をスキップ
|
|
937
1032
|
# ESCキーをそのまま handle_command_input に渡す
|
|
938
1033
|
if @command_mode_active
|
|
939
1034
|
handle_command_input(input)
|
|
940
|
-
return
|
|
1035
|
+
return true
|
|
941
1036
|
end
|
|
942
1037
|
|
|
943
1038
|
# 特殊キーの処理(エスケープシーケンス)(コマンドモード外のみ)
|
|
@@ -967,12 +1062,15 @@ module Rufio
|
|
|
967
1062
|
end
|
|
968
1063
|
|
|
969
1064
|
# キーバインドハンドラーに処理を委譲
|
|
970
|
-
@keybind_handler.handle_key(input) if input
|
|
1065
|
+
result = @keybind_handler.handle_key(input) if input
|
|
971
1066
|
|
|
972
|
-
# 終了処理(q
|
|
973
|
-
if input == 'q'
|
|
1067
|
+
# 終了処理(qキーのみ、確認ダイアログの結果を確認)
|
|
1068
|
+
if input == 'q' && result == true
|
|
974
1069
|
@running = false
|
|
975
1070
|
end
|
|
1071
|
+
|
|
1072
|
+
# 入力があったことを返す
|
|
1073
|
+
true
|
|
976
1074
|
end
|
|
977
1075
|
|
|
978
1076
|
def handle_input
|
|
@@ -1028,10 +1126,10 @@ module Rufio
|
|
|
1028
1126
|
end
|
|
1029
1127
|
|
|
1030
1128
|
# キーバインドハンドラーに処理を委譲
|
|
1031
|
-
|
|
1129
|
+
result = @keybind_handler.handle_key(input)
|
|
1032
1130
|
|
|
1033
|
-
# 終了処理(q
|
|
1034
|
-
if input == 'q'
|
|
1131
|
+
# 終了処理(qキーのみ、確認ダイアログの結果を確認)
|
|
1132
|
+
if input == 'q' && result == true
|
|
1035
1133
|
@running = false
|
|
1036
1134
|
end
|
|
1037
1135
|
end
|
data/lib/rufio/text_utils.rb
CHANGED
|
@@ -126,8 +126,17 @@ module Rufio
|
|
|
126
126
|
|
|
127
127
|
wrapped = []
|
|
128
128
|
lines.each do |line|
|
|
129
|
-
#
|
|
130
|
-
|
|
129
|
+
# Handle encoding errors: scrub invalid UTF-8 sequences
|
|
130
|
+
begin
|
|
131
|
+
# Force UTF-8 encoding and replace invalid bytes
|
|
132
|
+
line = line.encode('UTF-8', invalid: :replace, undef: :replace, replace: '?')
|
|
133
|
+
# Remove trailing whitespace
|
|
134
|
+
line = line.rstrip
|
|
135
|
+
rescue EncodingError, ArgumentError => e
|
|
136
|
+
# If encoding fails completely, skip this line
|
|
137
|
+
wrapped << '[encoding error]'
|
|
138
|
+
next
|
|
139
|
+
end
|
|
131
140
|
|
|
132
141
|
# If line is empty, keep it
|
|
133
142
|
if line.empty?
|
|
@@ -136,31 +145,45 @@ module Rufio
|
|
|
136
145
|
end
|
|
137
146
|
|
|
138
147
|
# If line fits within max_width, keep it as is
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
148
|
+
begin
|
|
149
|
+
if display_width(line) <= max_width
|
|
150
|
+
wrapped << line
|
|
151
|
+
next
|
|
152
|
+
end
|
|
153
|
+
rescue ArgumentError => e
|
|
154
|
+
# If display_width fails, just truncate by byte length
|
|
155
|
+
if line.bytesize <= max_width
|
|
156
|
+
wrapped << line
|
|
157
|
+
next
|
|
158
|
+
end
|
|
142
159
|
end
|
|
143
160
|
|
|
144
161
|
# Split long lines
|
|
145
162
|
current_line = []
|
|
146
163
|
current_width = 0
|
|
147
164
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
165
|
+
begin
|
|
166
|
+
line.each_char do |char|
|
|
167
|
+
cw = char_width(char)
|
|
168
|
+
|
|
169
|
+
if current_width + cw > max_width
|
|
170
|
+
# Start a new line
|
|
171
|
+
wrapped << current_line.join
|
|
172
|
+
current_line = [char]
|
|
173
|
+
current_width = cw
|
|
174
|
+
else
|
|
175
|
+
current_line << char
|
|
176
|
+
current_width += cw
|
|
177
|
+
end
|
|
159
178
|
end
|
|
160
|
-
end
|
|
161
179
|
|
|
162
|
-
|
|
163
|
-
|
|
180
|
+
# Add remaining characters
|
|
181
|
+
wrapped << current_line.join unless current_line.empty?
|
|
182
|
+
rescue ArgumentError, EncodingError => e
|
|
183
|
+
# If character iteration fails, just add the line truncated
|
|
184
|
+
truncated = line.byteslice(0, [max_width, line.bytesize].min)
|
|
185
|
+
wrapped << (truncated || line)
|
|
186
|
+
end
|
|
164
187
|
end
|
|
165
188
|
|
|
166
189
|
wrapped
|
data/lib/rufio/version.rb
CHANGED