rufio 0.34.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 +54 -0
- data/bin/rufio +17 -2
- data/docs/{CHANGELOG_v0.34.0.md → 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/color_helper.rb +59 -6
- 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/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 +2 -0
- metadata +7 -3
data/lib/rufio/terminal_ui.rb
CHANGED
|
@@ -32,7 +32,7 @@ module Rufio
|
|
|
32
32
|
# Line offsets
|
|
33
33
|
CONTENT_START_LINE = 2 # コンテンツ開始行(ヘッダー1行スキップ)
|
|
34
34
|
|
|
35
|
-
def initialize
|
|
35
|
+
def initialize(test_mode: false)
|
|
36
36
|
console = IO.console
|
|
37
37
|
if console
|
|
38
38
|
@screen_width, @screen_height = console.winsize.reverse
|
|
@@ -42,6 +42,7 @@ module Rufio
|
|
|
42
42
|
@screen_height = DEFAULT_SCREEN_HEIGHT
|
|
43
43
|
end
|
|
44
44
|
@running = false
|
|
45
|
+
@test_mode = test_mode
|
|
45
46
|
@command_mode_active = false
|
|
46
47
|
@command_input = ""
|
|
47
48
|
@command_mode = CommandMode.new
|
|
@@ -60,6 +61,16 @@ module Rufio
|
|
|
60
61
|
@project_log = nil
|
|
61
62
|
@in_project_mode = false
|
|
62
63
|
@in_log_mode = false
|
|
64
|
+
@project_mode_needs_redraw = false
|
|
65
|
+
|
|
66
|
+
# Preview cache
|
|
67
|
+
@preview_cache = {}
|
|
68
|
+
@last_preview_path = nil
|
|
69
|
+
|
|
70
|
+
# Footer cache (bookmark list)
|
|
71
|
+
@cached_bookmarks = nil
|
|
72
|
+
@cached_bookmark_time = nil
|
|
73
|
+
@bookmark_cache_ttl = 1.0 # 1秒間キャッシュ
|
|
63
74
|
end
|
|
64
75
|
|
|
65
76
|
def start(directory_listing, keybind_handler, file_preview, background_executor = nil)
|
|
@@ -90,6 +101,27 @@ module Rufio
|
|
|
90
101
|
# ウィンドウサイズを更新してから画面をクリアして再描画
|
|
91
102
|
update_screen_size
|
|
92
103
|
print "\e[2J\e[H" # clear screen, cursor to home
|
|
104
|
+
|
|
105
|
+
# プレビューキャッシュをクリア(ディレクトリ変更やリフレッシュ時)
|
|
106
|
+
@preview_cache.clear
|
|
107
|
+
@last_preview_path = nil
|
|
108
|
+
|
|
109
|
+
# ブックマークキャッシュもクリア
|
|
110
|
+
@cached_bookmarks = nil
|
|
111
|
+
@cached_bookmark_time = nil
|
|
112
|
+
|
|
113
|
+
# バッファベースの描画が利用可能な場合は全画面を再描画
|
|
114
|
+
if @screen && @renderer
|
|
115
|
+
# レンダラーの前フレーム情報をリセット(差分レンダリングを強制的に全体描画にする)
|
|
116
|
+
@renderer.clear
|
|
117
|
+
@screen.clear
|
|
118
|
+
# プロジェクトモードの場合は再描画フラグを立てる
|
|
119
|
+
@project_mode_needs_redraw = true if @in_project_mode
|
|
120
|
+
draw_screen_to_buffer(@screen, nil, nil)
|
|
121
|
+
@renderer.render(@screen)
|
|
122
|
+
# カーソルを画面外に移動
|
|
123
|
+
print "\e[#{@screen_height};#{@screen_width}H"
|
|
124
|
+
end
|
|
93
125
|
end
|
|
94
126
|
|
|
95
127
|
private
|
|
@@ -100,6 +132,11 @@ module Rufio
|
|
|
100
132
|
system('tput civis') # cursor invisible
|
|
101
133
|
print "\e[2J\e[H" # clear screen, cursor to home (first time only)
|
|
102
134
|
|
|
135
|
+
# rawモードに設定(ゲームループのノンブロッキング入力用)
|
|
136
|
+
if STDIN.tty?
|
|
137
|
+
STDIN.raw!
|
|
138
|
+
end
|
|
139
|
+
|
|
103
140
|
# re-acquire terminal size (just in case)
|
|
104
141
|
update_screen_size
|
|
105
142
|
end
|
|
@@ -112,17 +149,41 @@ module Rufio
|
|
|
112
149
|
end
|
|
113
150
|
|
|
114
151
|
def cleanup_terminal
|
|
152
|
+
# rawモードを解除
|
|
153
|
+
if STDIN.tty?
|
|
154
|
+
STDIN.cooked!
|
|
155
|
+
end
|
|
156
|
+
|
|
115
157
|
system('tput rmcup') # normal screen
|
|
116
158
|
system('tput cnorm') # cursor normal
|
|
117
159
|
puts ConfigLoader.message('app.terminated')
|
|
118
160
|
end
|
|
119
161
|
|
|
162
|
+
# ゲームループパターンのmain_loop
|
|
163
|
+
# UPDATE → DRAW → RENDER → SLEEP のサイクル
|
|
120
164
|
def main_loop
|
|
165
|
+
fps = 60
|
|
166
|
+
interval = 1.0 / fps
|
|
167
|
+
|
|
168
|
+
# Phase 3: Screen/Rendererを初期化
|
|
169
|
+
@screen = Screen.new(@screen_width, @screen_height)
|
|
170
|
+
@renderer = Renderer.new(@screen_width, @screen_height)
|
|
171
|
+
|
|
121
172
|
last_notification_check = Time.now
|
|
122
173
|
notification_message = nil
|
|
123
174
|
notification_time = nil
|
|
124
175
|
|
|
176
|
+
# FPS計測用
|
|
177
|
+
frame_times = []
|
|
178
|
+
last_frame_time = Time.now
|
|
179
|
+
current_fps = 0.0
|
|
180
|
+
|
|
125
181
|
while @running
|
|
182
|
+
start = Time.now
|
|
183
|
+
|
|
184
|
+
# UPDATE phase - ノンブロッキング入力処理
|
|
185
|
+
handle_input_nonblocking
|
|
186
|
+
|
|
126
187
|
# バックグラウンドコマンドの完了チェック(0.5秒ごと)
|
|
127
188
|
if @background_executor && (Time.now - last_notification_check) > 0.5
|
|
128
189
|
if !@background_executor.running? && @background_executor.get_completion_message
|
|
@@ -133,15 +194,41 @@ module Rufio
|
|
|
133
194
|
last_notification_check = Time.now
|
|
134
195
|
end
|
|
135
196
|
|
|
136
|
-
#
|
|
197
|
+
# FPS計算(移動平均)
|
|
198
|
+
if @test_mode
|
|
199
|
+
frame_time = Time.now - last_frame_time
|
|
200
|
+
frame_times << frame_time
|
|
201
|
+
frame_times.shift if frame_times.size > 60 # 直近60フレームで平均
|
|
202
|
+
avg_frame_time = frame_times.sum / frame_times.size
|
|
203
|
+
current_fps = 1.0 / avg_frame_time if avg_frame_time > 0
|
|
204
|
+
last_frame_time = Time.now
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# DRAW phase - Screenバッファに描画
|
|
208
|
+
@screen.clear
|
|
137
209
|
if notification_message && (Time.now - notification_time) < 3.0
|
|
138
|
-
|
|
210
|
+
draw_screen_to_buffer(@screen, notification_message, current_fps)
|
|
139
211
|
else
|
|
140
212
|
notification_message = nil if notification_message
|
|
141
|
-
|
|
213
|
+
draw_screen_to_buffer(@screen, nil, current_fps)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# RENDER phase - 差分レンダリング
|
|
217
|
+
@renderer.render(@screen)
|
|
218
|
+
|
|
219
|
+
# コマンドモードがアクティブな場合はフローティングウィンドウを表示
|
|
220
|
+
# Phase 4: 暫定的に直接描画(Screenバッファ外)
|
|
221
|
+
if @command_mode_active
|
|
222
|
+
@command_mode_ui.show_input_prompt(@command_input)
|
|
223
|
+
else
|
|
224
|
+
# カーソルを画面外に移動
|
|
225
|
+
print "\e[#{@screen_height};#{@screen_width}H"
|
|
142
226
|
end
|
|
143
227
|
|
|
144
|
-
|
|
228
|
+
# SLEEP phase - FPS制御
|
|
229
|
+
elapsed = Time.now - start
|
|
230
|
+
sleep_time = [interval - elapsed, 0].max
|
|
231
|
+
sleep sleep_time if sleep_time > 0
|
|
145
232
|
end
|
|
146
233
|
end
|
|
147
234
|
|
|
@@ -190,6 +277,57 @@ module Rufio
|
|
|
190
277
|
end
|
|
191
278
|
end
|
|
192
279
|
|
|
280
|
+
# Phase 3: Screenバッファに描画する新しいメソッド
|
|
281
|
+
def draw_screen_to_buffer(screen, notification_message = nil, fps = nil)
|
|
282
|
+
# プロジェクトモードの場合は既存の描画メソッドを使用(Phase 3では未実装)
|
|
283
|
+
if @in_project_mode
|
|
284
|
+
# プロジェクトモード用のバッファ描画は今後実装予定
|
|
285
|
+
# 現在は既存のdraw_project_mode_screenを直接呼び出す
|
|
286
|
+
# 注: レンダラーのクリアは状態遷移時のみ行う(set_project_mode等)
|
|
287
|
+
# ちらつき防止: 再描画が必要な時だけ描画
|
|
288
|
+
if @project_mode_needs_redraw
|
|
289
|
+
draw_project_mode_screen
|
|
290
|
+
@project_mode_needs_redraw = false
|
|
291
|
+
end
|
|
292
|
+
return
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# header (1 line) - y=0
|
|
296
|
+
draw_header_to_buffer(screen, 0)
|
|
297
|
+
|
|
298
|
+
# main content (left: directory list, right: preview)
|
|
299
|
+
entries = get_display_entries
|
|
300
|
+
selected_entry = entries[@keybind_handler.current_index]
|
|
301
|
+
|
|
302
|
+
# calculate height with header and footer margin
|
|
303
|
+
content_height = @screen_height - HEADER_FOOTER_MARGIN
|
|
304
|
+
left_width = (@screen_width * LEFT_PANEL_RATIO).to_i
|
|
305
|
+
right_width = @screen_width - left_width
|
|
306
|
+
|
|
307
|
+
# adjust so right panel doesn't overflow into left panel
|
|
308
|
+
right_width = @screen_width - left_width if left_width + right_width > @screen_width
|
|
309
|
+
|
|
310
|
+
draw_directory_list_to_buffer(screen, entries, left_width, content_height)
|
|
311
|
+
draw_file_preview_to_buffer(screen, selected_entry, right_width, content_height, left_width)
|
|
312
|
+
|
|
313
|
+
# footer
|
|
314
|
+
draw_footer_to_buffer(screen, @screen_height - 1, fps)
|
|
315
|
+
|
|
316
|
+
# 通知メッセージがある場合は表示
|
|
317
|
+
if notification_message
|
|
318
|
+
notification_line = @screen_height - 1
|
|
319
|
+
message_display = " #{notification_message} "
|
|
320
|
+
if message_display.length > @screen_width
|
|
321
|
+
message_display = message_display[0...(@screen_width - 3)] + "..."
|
|
322
|
+
end
|
|
323
|
+
screen.put_string(0, notification_line, message_display.ljust(@screen_width), fg: "\e[7m")
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# コマンドモードがアクティブな場合はフローティングウィンドウを描画
|
|
327
|
+
# Phase 4: 暫定的に既存のメソッドを使用
|
|
328
|
+
# Phase 5でScreenバッファ統合予定
|
|
329
|
+
end
|
|
330
|
+
|
|
193
331
|
def draw_screen_with_notification(notification_message)
|
|
194
332
|
# 通常の画面を描画
|
|
195
333
|
draw_screen
|
|
@@ -207,6 +345,42 @@ module Rufio
|
|
|
207
345
|
print "\e[7m#{message_display.ljust(@screen_width)}\e[0m"
|
|
208
346
|
end
|
|
209
347
|
|
|
348
|
+
# Phase 3: Screenバッファにヘッダーを描画
|
|
349
|
+
def draw_header_to_buffer(screen, y)
|
|
350
|
+
current_path = @directory_listing.current_path
|
|
351
|
+
header = "📁 rufio - #{current_path}"
|
|
352
|
+
|
|
353
|
+
# Add help mode indicator if in help mode
|
|
354
|
+
if @keybind_handler.help_mode?
|
|
355
|
+
header += " [Help Mode - Press ESC to exit]"
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# Add filter indicator if in filter mode
|
|
359
|
+
if @keybind_handler.filter_active?
|
|
360
|
+
filter_text = " [Filter: #{@keybind_handler.filter_query}]"
|
|
361
|
+
header += filter_text
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# abbreviate if path is too long
|
|
365
|
+
if header.length > @screen_width - HEADER_PADDING
|
|
366
|
+
if @keybind_handler.help_mode?
|
|
367
|
+
# prioritize showing help mode indicator
|
|
368
|
+
help_text = " [Help Mode - Press ESC to exit]"
|
|
369
|
+
base_length = @screen_width - help_text.length - FILTER_TEXT_RESERVED
|
|
370
|
+
header = "📁 rufio - ...#{current_path[-base_length..-1]}#{help_text}"
|
|
371
|
+
elsif @keybind_handler.filter_active?
|
|
372
|
+
# prioritize showing filter when active
|
|
373
|
+
filter_text = " [Filter: #{@keybind_handler.filter_query}]"
|
|
374
|
+
base_length = @screen_width - filter_text.length - FILTER_TEXT_RESERVED
|
|
375
|
+
header = "📁 rufio - ...#{current_path[-base_length..-1]}#{filter_text}"
|
|
376
|
+
else
|
|
377
|
+
header = "📁 rufio - ...#{current_path[-(@screen_width - FILTER_TEXT_RESERVED)..-1]}"
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
screen.put_string(0, y, header.ljust(@screen_width), fg: "\e[7m")
|
|
382
|
+
end
|
|
383
|
+
|
|
210
384
|
def draw_header
|
|
211
385
|
current_path = @directory_listing.current_path
|
|
212
386
|
header = "📁 rufio - #{current_path}"
|
|
@@ -244,6 +418,27 @@ module Rufio
|
|
|
244
418
|
|
|
245
419
|
|
|
246
420
|
|
|
421
|
+
# Phase 3: Screenバッファにディレクトリリストを描画
|
|
422
|
+
def draw_directory_list_to_buffer(screen, entries, width, height)
|
|
423
|
+
start_index = [@keybind_handler.current_index - height / 2, 0].max
|
|
424
|
+
|
|
425
|
+
(0...height).each do |i|
|
|
426
|
+
entry_index = start_index + i
|
|
427
|
+
line_num = i + CONTENT_START_LINE
|
|
428
|
+
|
|
429
|
+
if entry_index < entries.length
|
|
430
|
+
entry = entries[entry_index]
|
|
431
|
+
is_selected = entry_index == @keybind_handler.current_index
|
|
432
|
+
|
|
433
|
+
draw_entry_line_to_buffer(screen, entry, width, is_selected, 0, line_num)
|
|
434
|
+
else
|
|
435
|
+
# 空行
|
|
436
|
+
safe_width = [width - CURSOR_OFFSET, (@screen_width * LEFT_PANEL_RATIO).to_i - CURSOR_OFFSET].min
|
|
437
|
+
screen.put_string(0, line_num, ' ' * safe_width)
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
|
|
247
442
|
def draw_directory_list(entries, width, height)
|
|
248
443
|
start_index = [@keybind_handler.current_index - height / 2, 0].max
|
|
249
444
|
[start_index + height - 1, entries.length - 1].min
|
|
@@ -267,6 +462,50 @@ module Rufio
|
|
|
267
462
|
end
|
|
268
463
|
end
|
|
269
464
|
|
|
465
|
+
# Phase 3: Screenバッファにエントリ行を描画
|
|
466
|
+
def draw_entry_line_to_buffer(screen, entry, width, is_selected, x, y)
|
|
467
|
+
# アイコンと色の設定
|
|
468
|
+
icon, color = get_entry_display_info(entry)
|
|
469
|
+
|
|
470
|
+
# 左ペイン専用の安全な幅を計算
|
|
471
|
+
safe_width = [width - CURSOR_OFFSET, (@screen_width * LEFT_PANEL_RATIO).to_i - CURSOR_OFFSET].min
|
|
472
|
+
|
|
473
|
+
# 選択マークの追加
|
|
474
|
+
selection_mark = @keybind_handler.is_selected?(entry[:name]) ? "✓ " : " "
|
|
475
|
+
|
|
476
|
+
# ファイル名(必要に応じて切り詰め)
|
|
477
|
+
name = entry[:name]
|
|
478
|
+
max_name_length = safe_width - ICON_SIZE_PADDING
|
|
479
|
+
name = name[0...max_name_length - 3] + '...' if max_name_length > 0 && name.length > max_name_length
|
|
480
|
+
|
|
481
|
+
# サイズ情報
|
|
482
|
+
size_info = format_size(entry[:size])
|
|
483
|
+
|
|
484
|
+
# 行の内容を構築
|
|
485
|
+
content_without_size = "#{selection_mark}#{icon} #{name}"
|
|
486
|
+
available_for_content = safe_width - size_info.length
|
|
487
|
+
|
|
488
|
+
line_content = if available_for_content > 0
|
|
489
|
+
content_without_size.ljust(available_for_content) + size_info
|
|
490
|
+
else
|
|
491
|
+
content_without_size
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
# 確実に safe_width を超えないよう切り詰め
|
|
495
|
+
line_content = line_content[0...safe_width]
|
|
496
|
+
|
|
497
|
+
# 色を決定
|
|
498
|
+
if is_selected
|
|
499
|
+
fg_color = ColorHelper.color_to_selected_ansi(ConfigLoader.colors[:selected])
|
|
500
|
+
screen.put_string(x, y, line_content, fg: fg_color)
|
|
501
|
+
elsif @keybind_handler.is_selected?(entry[:name])
|
|
502
|
+
# 選択されたアイテムは緑背景、黒文字
|
|
503
|
+
screen.put_string(x, y, line_content, fg: "\e[42m\e[30m")
|
|
504
|
+
else
|
|
505
|
+
screen.put_string(x, y, line_content, fg: color)
|
|
506
|
+
end
|
|
507
|
+
end
|
|
508
|
+
|
|
270
509
|
def draw_entry_line(entry, width, is_selected)
|
|
271
510
|
# アイコンと色の設定
|
|
272
511
|
icon, color = get_entry_display_info(entry)
|
|
@@ -351,19 +590,124 @@ module Rufio
|
|
|
351
590
|
end
|
|
352
591
|
end
|
|
353
592
|
|
|
593
|
+
# Phase 3: Screenバッファにファイルプレビューを描画
|
|
594
|
+
def draw_file_preview_to_buffer(screen, selected_entry, width, height, left_offset)
|
|
595
|
+
# 事前計算
|
|
596
|
+
cursor_position = left_offset + CURSOR_OFFSET
|
|
597
|
+
max_chars_from_cursor = @screen_width - cursor_position
|
|
598
|
+
safe_width = [max_chars_from_cursor - 2, width - 2, 0].max
|
|
599
|
+
|
|
600
|
+
# プレビューコンテンツをキャッシュから取得(毎フレームのファイルI/Oを回避)
|
|
601
|
+
preview_content = nil
|
|
602
|
+
wrapped_lines = nil
|
|
603
|
+
|
|
604
|
+
if selected_entry && selected_entry[:type] == 'file'
|
|
605
|
+
# キャッシュチェック: 選択ファイルが変わった場合のみプレビューを更新
|
|
606
|
+
if @last_preview_path != selected_entry[:path]
|
|
607
|
+
preview_content = get_preview_content(selected_entry)
|
|
608
|
+
@preview_cache[selected_entry[:path]] = {
|
|
609
|
+
content: preview_content,
|
|
610
|
+
wrapped: {} # 幅ごとにキャッシュ
|
|
611
|
+
}
|
|
612
|
+
@last_preview_path = selected_entry[:path]
|
|
613
|
+
else
|
|
614
|
+
# キャッシュから取得
|
|
615
|
+
cache_entry = @preview_cache[selected_entry[:path]]
|
|
616
|
+
preview_content = cache_entry[:content] if cache_entry
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
# 折り返し処理もキャッシュ
|
|
620
|
+
if preview_content && safe_width > 0
|
|
621
|
+
cache_entry = @preview_cache[selected_entry[:path]]
|
|
622
|
+
if cache_entry && cache_entry[:wrapped][safe_width]
|
|
623
|
+
wrapped_lines = cache_entry[:wrapped][safe_width]
|
|
624
|
+
else
|
|
625
|
+
wrapped_lines = TextUtils.wrap_preview_lines(preview_content, safe_width - 1)
|
|
626
|
+
cache_entry[:wrapped][safe_width] = wrapped_lines if cache_entry
|
|
627
|
+
end
|
|
628
|
+
end
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
(0...height).each do |i|
|
|
632
|
+
line_num = i + CONTENT_START_LINE
|
|
633
|
+
|
|
634
|
+
# 区切り線
|
|
635
|
+
screen.put(cursor_position, line_num, '│')
|
|
636
|
+
|
|
637
|
+
content_to_print = ''
|
|
638
|
+
|
|
639
|
+
if selected_entry && i == 0
|
|
640
|
+
# プレビューヘッダー
|
|
641
|
+
header = " #{selected_entry[:name]} "
|
|
642
|
+
if @keybind_handler&.preview_focused?
|
|
643
|
+
header += "[PREVIEW MODE]"
|
|
644
|
+
end
|
|
645
|
+
content_to_print = header
|
|
646
|
+
elsif wrapped_lines && i >= 2
|
|
647
|
+
# ファイルプレビュー(折り返し対応)
|
|
648
|
+
scroll_offset = @keybind_handler&.preview_scroll_offset || 0
|
|
649
|
+
display_line_index = i - 2 + scroll_offset
|
|
650
|
+
|
|
651
|
+
if display_line_index < wrapped_lines.length
|
|
652
|
+
line = wrapped_lines[display_line_index] || ''
|
|
653
|
+
content_to_print = " #{line}"
|
|
654
|
+
else
|
|
655
|
+
content_to_print = ' '
|
|
656
|
+
end
|
|
657
|
+
else
|
|
658
|
+
content_to_print = ' '
|
|
659
|
+
end
|
|
660
|
+
|
|
661
|
+
# safe_widthを超えないよう切り詰め
|
|
662
|
+
next if safe_width <= 0
|
|
663
|
+
|
|
664
|
+
if TextUtils.display_width(content_to_print) > safe_width
|
|
665
|
+
content_to_print = TextUtils.truncate_to_width(content_to_print, safe_width)
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
# パディングを追加
|
|
669
|
+
remaining_space = safe_width - TextUtils.display_width(content_to_print)
|
|
670
|
+
content_to_print += ' ' * remaining_space if remaining_space > 0
|
|
671
|
+
|
|
672
|
+
screen.put_string(cursor_position + 1, line_num, content_to_print)
|
|
673
|
+
end
|
|
674
|
+
end
|
|
675
|
+
|
|
354
676
|
def draw_file_preview(selected_entry, width, height, left_offset)
|
|
355
677
|
# 事前計算(ループの外で一度だけ)
|
|
356
678
|
cursor_position = left_offset + CURSOR_OFFSET
|
|
357
679
|
max_chars_from_cursor = @screen_width - cursor_position
|
|
358
680
|
safe_width = [max_chars_from_cursor - 2, width - 2, 0].max
|
|
359
681
|
|
|
360
|
-
#
|
|
682
|
+
# プレビューコンテンツをキャッシュから取得(毎フレームのファイルI/Oを回避)
|
|
361
683
|
preview_content = nil
|
|
362
684
|
wrapped_lines = nil
|
|
363
685
|
|
|
364
686
|
if selected_entry && selected_entry[:type] == 'file'
|
|
365
|
-
|
|
366
|
-
|
|
687
|
+
# キャッシュチェック: 選択ファイルが変わった場合のみプレビューを更新
|
|
688
|
+
if @last_preview_path != selected_entry[:path]
|
|
689
|
+
preview_content = get_preview_content(selected_entry)
|
|
690
|
+
@preview_cache[selected_entry[:path]] = {
|
|
691
|
+
content: preview_content,
|
|
692
|
+
wrapped: {} # 幅ごとにキャッシュ
|
|
693
|
+
}
|
|
694
|
+
@last_preview_path = selected_entry[:path]
|
|
695
|
+
else
|
|
696
|
+
# キャッシュから取得
|
|
697
|
+
cache_entry = @preview_cache[selected_entry[:path]]
|
|
698
|
+
preview_content = cache_entry[:content] if cache_entry
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
# 折り返し処理もキャッシュ
|
|
702
|
+
if preview_content && safe_width > 0
|
|
703
|
+
cache_entry = @preview_cache[selected_entry[:path]]
|
|
704
|
+
if cache_entry && cache_entry[:wrapped][safe_width]
|
|
705
|
+
wrapped_lines = cache_entry[:wrapped][safe_width]
|
|
706
|
+
else
|
|
707
|
+
wrapped_lines = TextUtils.wrap_preview_lines(preview_content, safe_width - 1)
|
|
708
|
+
cache_entry[:wrapped][safe_width] = wrapped_lines if cache_entry
|
|
709
|
+
end
|
|
710
|
+
end
|
|
367
711
|
end
|
|
368
712
|
|
|
369
713
|
(0...height).each do |i|
|
|
@@ -447,6 +791,67 @@ module Rufio
|
|
|
447
791
|
end
|
|
448
792
|
end
|
|
449
793
|
|
|
794
|
+
# Phase 3: Screenバッファにフッターを描画
|
|
795
|
+
def draw_footer_to_buffer(screen, y, fps = nil)
|
|
796
|
+
if @keybind_handler.filter_active?
|
|
797
|
+
if @keybind_handler.instance_variable_get(:@filter_mode)
|
|
798
|
+
help_text = "Filter mode: Type to filter, ESC to clear, Enter to apply, Backspace to delete"
|
|
799
|
+
else
|
|
800
|
+
help_text = "Filtered view active - Space to edit filter, ESC to clear filter"
|
|
801
|
+
end
|
|
802
|
+
# フィルタモードでは通常のフッタを表示
|
|
803
|
+
footer_content = help_text.ljust(@screen_width)[0...@screen_width]
|
|
804
|
+
screen.put_string(0, y, footer_content, fg: "\e[7m")
|
|
805
|
+
else
|
|
806
|
+
# 通常モードではブックマーク一覧、ステータス情報、?:helpを1行に表示
|
|
807
|
+
# ブックマークをキャッシュ(毎フレームのファイルI/Oを回避)
|
|
808
|
+
current_time = Time.now
|
|
809
|
+
if @cached_bookmarks.nil? || @cached_bookmark_time.nil? || (current_time - @cached_bookmark_time) > @bookmark_cache_ttl
|
|
810
|
+
require_relative 'bookmark'
|
|
811
|
+
bookmark = Bookmark.new
|
|
812
|
+
@cached_bookmarks = bookmark.list
|
|
813
|
+
@cached_bookmark_time = current_time
|
|
814
|
+
end
|
|
815
|
+
bookmarks = @cached_bookmarks
|
|
816
|
+
|
|
817
|
+
# 起動ディレクトリを取得
|
|
818
|
+
start_dir = @directory_listing&.start_directory
|
|
819
|
+
start_dir_name = if start_dir
|
|
820
|
+
File.basename(start_dir)
|
|
821
|
+
else
|
|
822
|
+
"start"
|
|
823
|
+
end
|
|
824
|
+
|
|
825
|
+
# ブックマーク一覧を作成(0.起動dir を先頭に追加)
|
|
826
|
+
bookmark_parts = ["0.#{start_dir_name}"]
|
|
827
|
+
unless bookmarks.empty?
|
|
828
|
+
bookmark_parts.concat(bookmarks.take(9).map.with_index(1) { |bm, idx| "#{idx}.#{bm[:name]}" })
|
|
829
|
+
end
|
|
830
|
+
bookmark_text = bookmark_parts.join(" ")
|
|
831
|
+
|
|
832
|
+
# 右側の情報: FPS(test modeの時のみ)| ?:help
|
|
833
|
+
if @test_mode && fps
|
|
834
|
+
right_info = "#{fps.round(1)} FPS | ?:help"
|
|
835
|
+
else
|
|
836
|
+
right_info = "?:help"
|
|
837
|
+
end
|
|
838
|
+
|
|
839
|
+
# ブックマーク一覧を利用可能な幅に収める
|
|
840
|
+
available_width = @screen_width - right_info.length - 3
|
|
841
|
+
if bookmark_text.length > available_width && available_width > 3
|
|
842
|
+
bookmark_text = bookmark_text[0...available_width - 3] + "..."
|
|
843
|
+
elsif available_width <= 3
|
|
844
|
+
bookmark_text = ""
|
|
845
|
+
end
|
|
846
|
+
|
|
847
|
+
# フッタ全体を構築
|
|
848
|
+
padding = @screen_width - bookmark_text.length - right_info.length
|
|
849
|
+
footer_content = "#{bookmark_text}#{' ' * padding}#{right_info}"
|
|
850
|
+
footer_content = footer_content.ljust(@screen_width)[0...@screen_width]
|
|
851
|
+
screen.put_string(0, y, footer_content, fg: "\e[7m")
|
|
852
|
+
end
|
|
853
|
+
end
|
|
854
|
+
|
|
450
855
|
def draw_footer(render_time = nil)
|
|
451
856
|
# フッタは最下行に表示
|
|
452
857
|
footer_line = @screen_height - FOOTER_HEIGHT + 1
|
|
@@ -463,9 +868,15 @@ module Rufio
|
|
|
463
868
|
print "\e[7m#{footer_content}\e[0m"
|
|
464
869
|
else
|
|
465
870
|
# 通常モードではブックマーク一覧、ステータス情報、?:helpを1行に表示
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
871
|
+
# ブックマークをキャッシュ(毎フレームのファイルI/Oを回避)
|
|
872
|
+
current_time = Time.now
|
|
873
|
+
if @cached_bookmarks.nil? || @cached_bookmark_time.nil? || (current_time - @cached_bookmark_time) > @bookmark_cache_ttl
|
|
874
|
+
require_relative 'bookmark'
|
|
875
|
+
bookmark = Bookmark.new
|
|
876
|
+
@cached_bookmarks = bookmark.list
|
|
877
|
+
@cached_bookmark_time = current_time
|
|
878
|
+
end
|
|
879
|
+
bookmarks = @cached_bookmarks
|
|
469
880
|
|
|
470
881
|
# 起動ディレクトリを取得
|
|
471
882
|
start_dir = @directory_listing&.start_directory
|
|
@@ -504,6 +915,66 @@ module Rufio
|
|
|
504
915
|
end
|
|
505
916
|
end
|
|
506
917
|
|
|
918
|
+
# ノンブロッキング入力処理(ゲームループ用)
|
|
919
|
+
# IO.selectでタイムアウト付きで入力をチェック
|
|
920
|
+
def handle_input_nonblocking
|
|
921
|
+
# 1msタイムアウトで入力待ち(60FPS = 16.67ms/frame)
|
|
922
|
+
ready = IO.select([STDIN], nil, nil, 0.001)
|
|
923
|
+
return unless ready
|
|
924
|
+
|
|
925
|
+
begin
|
|
926
|
+
# read_nonblockを使ってノンブロッキングで1文字読み取る
|
|
927
|
+
input = STDIN.read_nonblock(1)
|
|
928
|
+
rescue IO::WaitReadable, IO::EAGAINWaitReadable
|
|
929
|
+
# 入力が利用できない
|
|
930
|
+
return
|
|
931
|
+
rescue Errno::ENOTTY, Errno::ENODEV
|
|
932
|
+
# ターミナルでない環境
|
|
933
|
+
return
|
|
934
|
+
end
|
|
935
|
+
|
|
936
|
+
# コマンドモードがアクティブな場合は、エスケープシーケンス処理をスキップ
|
|
937
|
+
# ESCキーをそのまま handle_command_input に渡す
|
|
938
|
+
if @command_mode_active
|
|
939
|
+
handle_command_input(input)
|
|
940
|
+
return
|
|
941
|
+
end
|
|
942
|
+
|
|
943
|
+
# 特殊キーの処理(エスケープシーケンス)(コマンドモード外のみ)
|
|
944
|
+
if input == "\e"
|
|
945
|
+
next_char = begin
|
|
946
|
+
STDIN.read_nonblock(1)
|
|
947
|
+
rescue StandardError
|
|
948
|
+
nil
|
|
949
|
+
end
|
|
950
|
+
if next_char == '['
|
|
951
|
+
# 矢印キーなどのシーケンス
|
|
952
|
+
third_char = begin
|
|
953
|
+
STDIN.read_nonblock(1)
|
|
954
|
+
rescue StandardError
|
|
955
|
+
nil
|
|
956
|
+
end
|
|
957
|
+
input = case third_char
|
|
958
|
+
when 'A' then 'k' # Up arrow
|
|
959
|
+
when 'B' then 'j' # Down arrow
|
|
960
|
+
when 'C' then 'l' # Right arrow
|
|
961
|
+
when 'D' then 'h' # Left arrow
|
|
962
|
+
else "\e" # ESCキー(そのまま保持)
|
|
963
|
+
end
|
|
964
|
+
else
|
|
965
|
+
input = "\e" # ESCキー(そのまま保持)
|
|
966
|
+
end
|
|
967
|
+
end
|
|
968
|
+
|
|
969
|
+
# キーバインドハンドラーに処理を委譲
|
|
970
|
+
@keybind_handler.handle_key(input) if input
|
|
971
|
+
|
|
972
|
+
# 終了処理(qキーのみ)
|
|
973
|
+
if input == 'q'
|
|
974
|
+
@running = false
|
|
975
|
+
end
|
|
976
|
+
end
|
|
977
|
+
|
|
507
978
|
def handle_input
|
|
508
979
|
begin
|
|
509
980
|
input = STDIN.getch
|
|
@@ -518,7 +989,14 @@ module Rufio
|
|
|
518
989
|
return 'q'
|
|
519
990
|
end
|
|
520
991
|
|
|
521
|
-
#
|
|
992
|
+
# コマンドモードがアクティブな場合は、エスケープシーケンス処理をスキップ
|
|
993
|
+
# ESCキーをそのまま handle_command_input に渡す
|
|
994
|
+
if @command_mode_active
|
|
995
|
+
handle_command_input(input)
|
|
996
|
+
return
|
|
997
|
+
end
|
|
998
|
+
|
|
999
|
+
# 特殊キーの処理(コマンドモード外のみ)
|
|
522
1000
|
if input == "\e"
|
|
523
1001
|
# エスケープシーケンスの処理
|
|
524
1002
|
next_char = begin
|
|
@@ -549,12 +1027,6 @@ module Rufio
|
|
|
549
1027
|
end
|
|
550
1028
|
end
|
|
551
1029
|
|
|
552
|
-
# コマンドモードがアクティブな場合は、コマンド入力を処理
|
|
553
|
-
if @command_mode_active
|
|
554
|
-
handle_command_input(input)
|
|
555
|
-
return
|
|
556
|
-
end
|
|
557
|
-
|
|
558
1030
|
# キーバインドハンドラーに処理を委譲
|
|
559
1031
|
_result = @keybind_handler.handle_key(input)
|
|
560
1032
|
|
|
@@ -584,16 +1056,37 @@ module Rufio
|
|
|
584
1056
|
@command_mode_active
|
|
585
1057
|
end
|
|
586
1058
|
|
|
1059
|
+
# プロジェクトモードの再描画をトリガー
|
|
1060
|
+
def trigger_project_mode_redraw
|
|
1061
|
+
@project_mode_needs_redraw = true if @in_project_mode
|
|
1062
|
+
end
|
|
1063
|
+
|
|
587
1064
|
# コマンド入力を処理
|
|
588
1065
|
def handle_command_input(input)
|
|
589
1066
|
case input
|
|
590
1067
|
when "\r", "\n"
|
|
591
1068
|
# Enter キーでコマンドを実行
|
|
592
1069
|
execute_command(@command_input)
|
|
593
|
-
|
|
1070
|
+
# コマンド実行後、入力をクリアして再度コマンドモードに戻る
|
|
1071
|
+
@command_input = ""
|
|
594
1072
|
when "\e"
|
|
595
1073
|
# Escape キーでコマンドモードをキャンセル
|
|
1074
|
+
# まずコマンドウィンドウをクリア
|
|
1075
|
+
@command_mode_ui.clear_prompt
|
|
596
1076
|
deactivate_command_mode
|
|
1077
|
+
# ファイラー画面を再描画(バッファベース)
|
|
1078
|
+
if @screen && @renderer
|
|
1079
|
+
# レンダラーの前フレーム情報をリセット(差分レンダリングを強制的に全体描画にする)
|
|
1080
|
+
@renderer.clear
|
|
1081
|
+
@screen.clear
|
|
1082
|
+
draw_screen_to_buffer(@screen, nil, nil)
|
|
1083
|
+
@renderer.render(@screen)
|
|
1084
|
+
# カーソルを画面外に移動(メインループと同じ処理)
|
|
1085
|
+
print "\e[#{@screen_height};#{@screen_width}H"
|
|
1086
|
+
else
|
|
1087
|
+
# フォールバック(古い実装)
|
|
1088
|
+
draw_screen
|
|
1089
|
+
end
|
|
597
1090
|
when "\t"
|
|
598
1091
|
# Tab キーで補完
|
|
599
1092
|
handle_tab_completion
|
|
@@ -623,7 +1116,12 @@ module Rufio
|
|
|
623
1116
|
end
|
|
624
1117
|
|
|
625
1118
|
# 画面を再描画
|
|
626
|
-
|
|
1119
|
+
if @in_project_mode
|
|
1120
|
+
# プロジェクトモードの場合は再描画フラグを立てる
|
|
1121
|
+
@project_mode_needs_redraw = true
|
|
1122
|
+
else
|
|
1123
|
+
draw_screen
|
|
1124
|
+
end
|
|
627
1125
|
end
|
|
628
1126
|
|
|
629
1127
|
# Tab補完を処理
|
|
@@ -701,7 +1199,12 @@ module Rufio
|
|
|
701
1199
|
@dialog_renderer.clear_area(x, y, width, height)
|
|
702
1200
|
|
|
703
1201
|
# 画面を再描画
|
|
704
|
-
|
|
1202
|
+
if @in_project_mode
|
|
1203
|
+
# プロジェクトモードの場合は再描画フラグを立てる
|
|
1204
|
+
@project_mode_needs_redraw = true
|
|
1205
|
+
else
|
|
1206
|
+
draw_screen
|
|
1207
|
+
end
|
|
705
1208
|
end
|
|
706
1209
|
|
|
707
1210
|
# Show info notices from the info directory if any are unread
|
|
@@ -753,7 +1256,12 @@ module Rufio
|
|
|
753
1256
|
@dialog_renderer.clear_area(x, y, width, height)
|
|
754
1257
|
|
|
755
1258
|
# Redraw the screen
|
|
756
|
-
|
|
1259
|
+
if @in_project_mode
|
|
1260
|
+
# プロジェクトモードの場合は再描画フラグを立てる
|
|
1261
|
+
@project_mode_needs_redraw = true
|
|
1262
|
+
else
|
|
1263
|
+
draw_screen
|
|
1264
|
+
end
|
|
757
1265
|
end
|
|
758
1266
|
|
|
759
1267
|
# プロジェクトモードを設定
|
|
@@ -763,8 +1271,11 @@ module Rufio
|
|
|
763
1271
|
@project_log = project_log
|
|
764
1272
|
@in_project_mode = true
|
|
765
1273
|
@in_log_mode = false
|
|
766
|
-
|
|
767
|
-
|
|
1274
|
+
# 画面を一度クリアしてレンダラーをリセット
|
|
1275
|
+
print "\e[2J\e[H"
|
|
1276
|
+
@renderer.clear if @renderer
|
|
1277
|
+
# 再描画フラグを立てる
|
|
1278
|
+
@project_mode_needs_redraw = true
|
|
768
1279
|
end
|
|
769
1280
|
|
|
770
1281
|
# プロジェクトモードを終了
|
|
@@ -774,16 +1285,29 @@ module Rufio
|
|
|
774
1285
|
@project_mode = nil
|
|
775
1286
|
@project_command = nil
|
|
776
1287
|
@project_log = nil
|
|
777
|
-
|
|
778
|
-
|
|
1288
|
+
# バッファベースの全画面再描画を使用
|
|
1289
|
+
update_screen_size
|
|
1290
|
+
print "\e[2J\e[H"
|
|
1291
|
+
if @screen && @renderer
|
|
1292
|
+
@renderer.clear
|
|
1293
|
+
@screen.clear
|
|
1294
|
+
draw_screen_to_buffer(@screen, nil, nil)
|
|
1295
|
+
@renderer.render(@screen)
|
|
1296
|
+
print "\e[#{@screen_height};#{@screen_width}H"
|
|
1297
|
+
else
|
|
1298
|
+
draw_screen
|
|
1299
|
+
end
|
|
779
1300
|
end
|
|
780
1301
|
|
|
781
1302
|
# ログモードに入る
|
|
782
1303
|
def enter_log_mode(project_log)
|
|
783
1304
|
@in_log_mode = true
|
|
784
1305
|
@project_log = project_log
|
|
785
|
-
|
|
786
|
-
|
|
1306
|
+
# 画面を一度クリアしてレンダラーをリセット
|
|
1307
|
+
print "\e[2J\e[H"
|
|
1308
|
+
@renderer.clear if @renderer
|
|
1309
|
+
# 再描画フラグを立てる
|
|
1310
|
+
@project_mode_needs_redraw = true
|
|
787
1311
|
end
|
|
788
1312
|
|
|
789
1313
|
# プロジェクトモード画面を描画
|
|
@@ -1025,8 +1549,11 @@ module Rufio
|
|
|
1025
1549
|
# ログモードを終了してプロジェクトモードに戻る
|
|
1026
1550
|
def exit_log_mode
|
|
1027
1551
|
@in_log_mode = false
|
|
1028
|
-
|
|
1029
|
-
|
|
1552
|
+
# 画面を一度クリアしてレンダラーをリセット
|
|
1553
|
+
print "\e[2J\e[H"
|
|
1554
|
+
@renderer.clear if @renderer
|
|
1555
|
+
# 再描画フラグを立てる
|
|
1556
|
+
@project_mode_needs_redraw = true
|
|
1030
1557
|
end
|
|
1031
1558
|
|
|
1032
1559
|
# プロジェクト未選択メッセージ
|
|
@@ -1054,7 +1581,6 @@ module Rufio
|
|
|
1054
1581
|
|
|
1055
1582
|
# 画面を再描画
|
|
1056
1583
|
refresh_display
|
|
1057
|
-
draw_screen
|
|
1058
1584
|
end
|
|
1059
1585
|
|
|
1060
1586
|
# ヘルプダイアログを表示
|
|
@@ -1117,7 +1643,6 @@ module Rufio
|
|
|
1117
1643
|
|
|
1118
1644
|
# 画面を再描画
|
|
1119
1645
|
refresh_display
|
|
1120
|
-
draw_screen
|
|
1121
1646
|
end
|
|
1122
1647
|
|
|
1123
1648
|
# プロジェクトモードでコマンドを実行
|
|
@@ -1149,7 +1674,6 @@ module Rufio
|
|
|
1149
1674
|
|
|
1150
1675
|
# 画面を再描画
|
|
1151
1676
|
refresh_display
|
|
1152
|
-
draw_screen
|
|
1153
1677
|
end
|
|
1154
1678
|
|
|
1155
1679
|
# スクリプトまたはコマンドを選択
|
|
@@ -1297,7 +1821,6 @@ module Rufio
|
|
|
1297
1821
|
|
|
1298
1822
|
# 画面を再描画
|
|
1299
1823
|
refresh_display
|
|
1300
|
-
draw_screen
|
|
1301
1824
|
end
|
|
1302
1825
|
end
|
|
1303
1826
|
end
|