rufio 0.34.0 → 0.40.1

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.
@@ -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
- # 通知メッセージを表示(3秒間)
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
- draw_screen_with_notification(notification_message)
210
+ draw_screen_to_buffer(@screen, notification_message, current_fps)
139
211
  else
140
212
  notification_message = nil if notification_message
141
- draw_screen
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
- handle_input
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
- # プレビューコンテンツとWrapped linesを一度だけ計算
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
- preview_content = get_preview_content(selected_entry)
366
- wrapped_lines = TextUtils.wrap_preview_lines(preview_content, safe_width - 1) if safe_width > 0
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
- require_relative 'bookmark'
467
- bookmark = Bookmark.new
468
- bookmarks = bookmark.list
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
- deactivate_command_mode
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
- draw_screen
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
- draw_screen
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
- draw_screen
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
- refresh_display
767
- draw_screen
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
- refresh_display
778
- draw_screen
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
- refresh_display
786
- draw_screen
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
- refresh_display
1029
- draw_screen
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