rufio 0.63.0 → 0.65.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.
@@ -73,6 +73,9 @@ module Rufio
73
73
 
74
74
  def set_terminal_ui(terminal_ui)
75
75
  @terminal_ui = terminal_ui
76
+ # terminal_ui が設定されたら、bookmark_manager と zoxide_integration にも渡す
77
+ @bookmark_manager.set_terminal_ui(terminal_ui)
78
+ @zoxide_integration.set_terminal_ui(terminal_ui)
76
79
  end
77
80
 
78
81
  def selected_items
@@ -430,6 +433,54 @@ module Rufio
430
433
 
431
434
  private
432
435
 
436
+ # オーバーレイダイアログを表示してキー入力を待つヘルパーメソッド
437
+ # terminal_ui が利用可能な場合はオーバーレイを使用、そうでなければ従来の方法を使用
438
+ # @param title [String] ダイアログタイトル
439
+ # @param content_lines [Array<String>] コンテンツ行
440
+ # @param options [Hash] オプション
441
+ # @yield キー入力処理(ブロックが与えられた場合)
442
+ # @return [String] 入力されたキー
443
+ def show_overlay_dialog(title, content_lines, options = {}, &block)
444
+ # terminal_ui が利用可能で、screen と renderer が存在する場合のみオーバーレイを使用
445
+ use_overlay = @terminal_ui &&
446
+ @terminal_ui.respond_to?(:screen) &&
447
+ @terminal_ui.respond_to?(:renderer) &&
448
+ @terminal_ui.screen &&
449
+ @terminal_ui.renderer
450
+
451
+ if use_overlay
452
+ # オーバーレイを使用
453
+ @terminal_ui.show_overlay_dialog(title, content_lines, options, &block)
454
+ else
455
+ # フォールバック: 従来の方法
456
+ width = options[:width]
457
+ height = options[:height]
458
+
459
+ unless width && height
460
+ width, height = @dialog_renderer.calculate_dimensions(content_lines, {
461
+ title: title,
462
+ min_width: options[:min_width] || 40,
463
+ max_width: options[:max_width] || 80
464
+ })
465
+ end
466
+
467
+ x, y = @dialog_renderer.calculate_center(width, height)
468
+
469
+ @dialog_renderer.draw_floating_window(x, y, width, height, title, content_lines, {
470
+ border_color: options[:border_color] || "\e[37m",
471
+ title_color: options[:title_color] || "\e[1;33m",
472
+ content_color: options[:content_color] || "\e[37m"
473
+ })
474
+
475
+ key = block_given? ? yield : STDIN.getch
476
+
477
+ @dialog_renderer.clear_area(x, y, width, height)
478
+ @terminal_ui&.refresh_display
479
+
480
+ key
481
+ end
482
+ end
483
+
433
484
  # Enterキーの処理:ファイルならプレビューフォーカス、ディレクトリならナビゲート
434
485
  def handle_enter_key
435
486
  entry = current_entry
@@ -537,7 +588,8 @@ module Rufio
537
588
 
538
589
  if entry[:type] == 'file'
539
590
  @file_opener.open_file(entry[:path])
540
- true
591
+ # ターミナルアプリ(vim等)を起動した後は画面リフレッシュが必要
592
+ :needs_refresh
541
593
  else
542
594
  false
543
595
  end
@@ -571,7 +623,8 @@ module Rufio
571
623
  @file_opener.open_file(full_path) if File.exist?(full_path)
572
624
  end
573
625
 
574
- true
626
+ # fzfとvim等はターミナルを占有するので画面リフレッシュが必要
627
+ :needs_refresh
575
628
  end
576
629
 
577
630
  def fzf_available?
@@ -620,7 +673,8 @@ module Rufio
620
673
  @file_opener.open_file_with_line(full_path, line_number) if File.exist?(full_path)
621
674
  end
622
675
 
623
- true
676
+ # fzfとvim等はターミナルを占有するので画面リフレッシュが必要
677
+ :needs_refresh
624
678
  end
625
679
 
626
680
  def rga_available?
@@ -804,32 +858,31 @@ module Rufio
804
858
  title = 'Confirm Delete'
805
859
  width = [50, current_name.length + 10].max
806
860
  height = content_lines.length + 4
807
- x, y = @dialog_renderer.calculate_center(width, height)
808
861
 
809
- @dialog_renderer.draw_floating_window(x, y, width, height, title, content_lines, {
862
+ # 確認を待つ
863
+ confirmed = false
864
+ show_overlay_dialog(title, content_lines, {
865
+ width: width,
866
+ height: height,
810
867
  border_color: "\e[31m", # Red (warning)
811
868
  title_color: "\e[1;31m", # Bold red
812
869
  content_color: "\e[37m" # White
813
- })
814
-
815
- # 確認を待つ
816
- confirmed = false
817
- loop do
818
- input = STDIN.getch.downcase
819
-
820
- case input
821
- when 'y'
822
- confirmed = true
823
- break
824
- when 'n', "\e" # n or ESC
825
- confirmed = false
826
- break
870
+ }) do
871
+ loop do
872
+ input = STDIN.getch.downcase
873
+
874
+ case input
875
+ when 'y'
876
+ confirmed = true
877
+ break
878
+ when 'n', "\e" # n or ESC
879
+ confirmed = false
880
+ break
881
+ end
827
882
  end
883
+ nil
828
884
  end
829
885
 
830
- @dialog_renderer.clear_area(x, y, width, height)
831
- @terminal_ui&.refresh_display
832
-
833
886
  return false unless confirmed
834
887
 
835
888
  # FileOperationsを使用して削除
@@ -969,41 +1022,35 @@ module Rufio
969
1022
  # タイトルあり: 上枠1 + タイトル1 + 区切り1 + コンテンツ + 下枠1
970
1023
  dialog_height = DIALOG_BORDER_HEIGHT + content_lines.length
971
1024
 
972
- # ダイアログの位置を中央に設定
973
- x, y = @dialog_renderer.calculate_center(dialog_width, dialog_height)
974
-
975
- # ダイアログの描画
976
- @dialog_renderer.draw_floating_window(x, y, dialog_width, dialog_height, title, content_lines, {
977
- border_color: "\e[31m", # 赤色(警告)
978
- title_color: "\e[1;31m", # 太字赤色
979
- content_color: "\e[37m" # 白色
980
- })
981
-
982
1025
  # フラッシュしてユーザーの注意を引く
983
1026
  print "\a" # ベル音
984
1027
 
985
1028
  # キー入力待機
986
- loop do
987
- input = STDIN.getch.downcase
988
-
989
- case input
990
- when 'y'
991
- # ダイアログをクリア
992
- @dialog_renderer.clear_area(x, y, dialog_width, dialog_height)
993
- @terminal_ui&.refresh_display # 画面を再描画
994
- return true
995
- when 'n', "\e", "\x03" # n, ESC, Ctrl+C
996
- # ダイアログをクリア
997
- @dialog_renderer.clear_area(x, y, dialog_width, dialog_height)
998
- @terminal_ui&.refresh_display # 画面を再描画
999
- return false
1000
- when 'q' # qキーでもキャンセル
1001
- @dialog_renderer.clear_area(x, y, dialog_width, dialog_height)
1002
- @terminal_ui&.refresh_display
1003
- return false
1029
+ confirmed = false
1030
+ show_overlay_dialog(title, content_lines, {
1031
+ width: dialog_width,
1032
+ height: dialog_height,
1033
+ border_color: "\e[31m", # 赤色(警告)
1034
+ title_color: "\e[1;31m", # 太字赤色
1035
+ content_color: "\e[37m" # 白色
1036
+ }) do
1037
+ loop do
1038
+ input = STDIN.getch.downcase
1039
+
1040
+ case input
1041
+ when 'y'
1042
+ confirmed = true
1043
+ break
1044
+ when 'n', "\e", "\x03", 'q' # n, ESC, Ctrl+C, q
1045
+ confirmed = false
1046
+ break
1047
+ end
1048
+ # 無効なキー入力の場合は再度ループ
1004
1049
  end
1005
- # 無効なキー入力の場合は再度ループ
1050
+ nil
1006
1051
  end
1052
+
1053
+ confirmed
1007
1054
  end
1008
1055
 
1009
1056
  def show_floating_move_confirmation(count, source_path, dest_path)
@@ -1030,38 +1077,32 @@ module Rufio
1030
1077
  dialog_width = CONFIRMATION_DIALOG_WIDTH
1031
1078
  dialog_height = DIALOG_BORDER_HEIGHT + content_lines.length
1032
1079
 
1033
- # ダイアログの位置を中央に設定
1034
- x, y = @dialog_renderer.calculate_center(dialog_width, dialog_height)
1035
-
1036
- # ダイアログの描画(移動は青色で表示)
1037
- @dialog_renderer.draw_floating_window(x, y, dialog_width, dialog_height, title, content_lines, {
1038
- border_color: "\e[34m", # 青色(情報)
1039
- title_color: "\e[1;34m", # 太字青色
1040
- content_color: "\e[37m" # 白色
1041
- })
1042
-
1043
1080
  # キー入力待機
1044
- loop do
1045
- input = STDIN.getch.downcase
1046
-
1047
- case input
1048
- when 'y'
1049
- # ダイアログをクリア
1050
- @dialog_renderer.clear_area(x, y, dialog_width, dialog_height)
1051
- @terminal_ui&.refresh_display # 画面を再描画
1052
- return true
1053
- when 'n', "\e", "\x03" # n, ESC, Ctrl+C
1054
- # ダイアログをクリア
1055
- @dialog_renderer.clear_area(x, y, dialog_width, dialog_height)
1056
- @terminal_ui&.refresh_display # 画面を再描画
1057
- return false
1058
- when 'q' # qキーでもキャンセル
1059
- @dialog_renderer.clear_area(x, y, dialog_width, dialog_height)
1060
- @terminal_ui&.refresh_display
1061
- return false
1081
+ confirmed = false
1082
+ show_overlay_dialog(title, content_lines, {
1083
+ width: dialog_width,
1084
+ height: dialog_height,
1085
+ border_color: "\e[34m", # 青色(情報)
1086
+ title_color: "\e[1;34m", # 太字青色
1087
+ content_color: "\e[37m" # 白色
1088
+ }) do
1089
+ loop do
1090
+ input = STDIN.getch.downcase
1091
+
1092
+ case input
1093
+ when 'y'
1094
+ confirmed = true
1095
+ break
1096
+ when 'n', "\e", "\x03", 'q' # n, ESC, Ctrl+C, q
1097
+ confirmed = false
1098
+ break
1099
+ end
1100
+ # 無効なキー入力の場合は再度ループ
1062
1101
  end
1063
- # 無効なキー入力の場合は再度ループ
1102
+ nil
1064
1103
  end
1104
+
1105
+ confirmed
1065
1106
  end
1066
1107
 
1067
1108
  def show_floating_copy_confirmation(count, source_path, dest_path)
@@ -1088,38 +1129,32 @@ module Rufio
1088
1129
  dialog_width = CONFIRMATION_DIALOG_WIDTH
1089
1130
  dialog_height = DIALOG_BORDER_HEIGHT + content_lines.length
1090
1131
 
1091
- # ダイアログの位置を中央に設定
1092
- x, y = @dialog_renderer.calculate_center(dialog_width, dialog_height)
1093
-
1094
- # ダイアログの描画(コピーは緑色で表示)
1095
- @dialog_renderer.draw_floating_window(x, y, dialog_width, dialog_height, title, content_lines, {
1096
- border_color: "\e[32m", # 緑色(安全な操作)
1097
- title_color: "\e[1;32m", # 太字緑色
1098
- content_color: "\e[37m" # 白色
1099
- })
1100
-
1101
1132
  # キー入力待機
1102
- loop do
1103
- input = STDIN.getch.downcase
1104
-
1105
- case input
1106
- when 'y'
1107
- # ダイアログをクリア
1108
- @dialog_renderer.clear_area(x, y, dialog_width, dialog_height)
1109
- @terminal_ui&.refresh_display # 画面を再描画
1110
- return true
1111
- when 'n', "\e", "\x03" # n, ESC, Ctrl+C
1112
- # ダイアログをクリア
1113
- @dialog_renderer.clear_area(x, y, dialog_width, dialog_height)
1114
- @terminal_ui&.refresh_display # 画面を再描画
1115
- return false
1116
- when 'q' # qキーでもキャンセル
1117
- @dialog_renderer.clear_area(x, y, dialog_width, dialog_height)
1118
- @terminal_ui&.refresh_display
1119
- return false
1133
+ confirmed = false
1134
+ show_overlay_dialog(title, content_lines, {
1135
+ width: dialog_width,
1136
+ height: dialog_height,
1137
+ border_color: "\e[32m", # 緑色(安全な操作)
1138
+ title_color: "\e[1;32m", # 太字緑色
1139
+ content_color: "\e[37m" # 白色
1140
+ }) do
1141
+ loop do
1142
+ input = STDIN.getch.downcase
1143
+
1144
+ case input
1145
+ when 'y'
1146
+ confirmed = true
1147
+ break
1148
+ when 'n', "\e", "\x03", 'q' # n, ESC, Ctrl+C, q
1149
+ confirmed = false
1150
+ break
1151
+ end
1152
+ # 無効なキー入力の場合は再度ループ
1120
1153
  end
1121
- # 無効なキー入力の場合は再度ループ
1154
+ nil
1122
1155
  end
1156
+
1157
+ confirmed
1123
1158
  end
1124
1159
 
1125
1160
  def show_exit_confirmation
@@ -1139,34 +1174,32 @@ module Rufio
1139
1174
  dialog_width = CONFIRMATION_DIALOG_WIDTH
1140
1175
  dialog_height = DIALOG_BORDER_HEIGHT + content_lines.length
1141
1176
 
1142
- # ダイアログの位置を中央に設定
1143
- x, y = @dialog_renderer.calculate_center(dialog_width, dialog_height)
1144
-
1145
- # ダイアログの描画(終了は黄色で表示)
1146
- @dialog_renderer.draw_floating_window(x, y, dialog_width, dialog_height, title, content_lines, {
1147
- border_color: "\e[33m", # 黄色(注意)
1148
- title_color: "\e[1;33m", # 太字黄色
1149
- content_color: "\e[37m" # 白色
1150
- })
1151
-
1152
1177
  # キー入力待機
1153
- loop do
1154
- input = STDIN.getch.downcase
1155
-
1156
- case input
1157
- when 'y'
1158
- # ダイアログをクリア
1159
- @dialog_renderer.clear_area(x, y, dialog_width, dialog_height)
1160
- @terminal_ui&.refresh_display # 画面を再描画
1161
- return true
1162
- when 'n', "\e", "\x03" # n, ESC, Ctrl+C
1163
- # ダイアログをクリア
1164
- @dialog_renderer.clear_area(x, y, dialog_width, dialog_height)
1165
- @terminal_ui&.refresh_display # 画面を再描画
1166
- return false
1178
+ confirmed = false
1179
+ show_overlay_dialog(title, content_lines, {
1180
+ width: dialog_width,
1181
+ height: dialog_height,
1182
+ border_color: "\e[33m", # 黄色(注意)
1183
+ title_color: "\e[1;33m", # 太字黄色
1184
+ content_color: "\e[37m" # 白色
1185
+ }) do
1186
+ loop do
1187
+ input = STDIN.getch.downcase
1188
+
1189
+ case input
1190
+ when 'y'
1191
+ confirmed = true
1192
+ break
1193
+ when 'n', "\e", "\x03" # n, ESC, Ctrl+C
1194
+ confirmed = false
1195
+ break
1196
+ end
1197
+ # 無効なキー入力の場合は再度ループ
1167
1198
  end
1168
- # 無効なキー入力の場合は再度ループ
1199
+ nil
1169
1200
  end
1201
+
1202
+ confirmed
1170
1203
  end
1171
1204
 
1172
1205
  # パスを指定した長さに短縮
@@ -1249,9 +1282,6 @@ module Rufio
1249
1282
  dialog_width = has_errors ? 50 : 35
1250
1283
  dialog_height = has_errors ? [8 + error_messages.length, 15].min : 6
1251
1284
 
1252
- # ダイアログの位置を中央に設定
1253
- x, y = @dialog_renderer.calculate_center(dialog_width, dialog_height)
1254
-
1255
1285
  # 成功・失敗に応じた色設定
1256
1286
  if success_count == total_count && !has_errors
1257
1287
  border_color = "\e[32m" # 緑色(成功)
@@ -1284,19 +1314,14 @@ module Rufio
1284
1314
  content_lines << ''
1285
1315
  content_lines << 'Press any key to continue...'
1286
1316
 
1287
- # ダイアログの描画
1288
- @dialog_renderer.draw_floating_window(x, y, dialog_width, dialog_height, title, content_lines, {
1289
- border_color: border_color,
1290
- title_color: title_color,
1291
- content_color: "\e[37m"
1292
- })
1293
-
1294
- # キー入力待機
1295
- STDIN.getch
1296
-
1297
- # ダイアログをクリア
1298
- @dialog_renderer.clear_area(x, y, dialog_width, dialog_height)
1299
- @terminal_ui&.refresh_display
1317
+ # オーバーレイダイアログを表示
1318
+ show_overlay_dialog(title, content_lines, {
1319
+ width: dialog_width,
1320
+ height: dialog_height,
1321
+ border_color: border_color,
1322
+ title_color: title_color,
1323
+ content_color: "\e[37m"
1324
+ })
1300
1325
  end
1301
1326
 
1302
1327
 
@@ -1378,17 +1403,16 @@ module Rufio
1378
1403
  title = 'Bookmark Exists'
1379
1404
  width = 50
1380
1405
  height = content_lines.length + 4
1381
- x, y = @dialog_renderer.calculate_center(width, height)
1382
1406
 
1383
- @dialog_renderer.draw_floating_window(x, y, width, height, title, content_lines, {
1407
+ # オーバーレイダイアログを表示
1408
+ show_overlay_dialog(title, content_lines, {
1409
+ width: width,
1410
+ height: height,
1384
1411
  border_color: "\e[33m", # Yellow
1385
1412
  title_color: "\e[1;33m", # Bold yellow
1386
1413
  content_color: "\e[37m" # White
1387
1414
  })
1388
1415
 
1389
- STDIN.getch
1390
- @dialog_renderer.clear_area(x, y, width, height)
1391
- @terminal_ui&.refresh_display
1392
1416
  return false
1393
1417
  end
1394
1418
 
@@ -1429,17 +1453,16 @@ module Rufio
1429
1453
  title = 'Bookmark Menu'
1430
1454
  width = 45
1431
1455
  height = content_lines.length + 4
1432
- x, y = @dialog_renderer.calculate_center(width, height)
1433
1456
 
1434
- @dialog_renderer.draw_floating_window(x, y, width, height, title, content_lines, {
1457
+ # オーバーレイダイアログを表示してキー入力を取得
1458
+ key = show_overlay_dialog(title, content_lines, {
1459
+ width: width,
1460
+ height: height,
1435
1461
  border_color: "\e[36m", # Cyan
1436
1462
  title_color: "\e[1;36m", # Bold cyan
1437
1463
  content_color: "\e[37m" # White
1438
1464
  })
1439
1465
 
1440
- key = STDIN.getch
1441
- @dialog_renderer.clear_area(x, y, width, height)
1442
-
1443
1466
  case key
1444
1467
  when '1'
1445
1468
  add_bookmark
@@ -1503,6 +1526,8 @@ module Rufio
1503
1526
  end
1504
1527
 
1505
1528
  selected_index = 0
1529
+ screen = @terminal_ui&.screen
1530
+ renderer = @terminal_ui&.renderer
1506
1531
 
1507
1532
  loop do
1508
1533
  # メニューを描画
@@ -1516,16 +1541,33 @@ module Rufio
1516
1541
  title = 'Script Paths'
1517
1542
  width = 50
1518
1543
  height = content_lines.length + 4
1519
- x, y = @dialog_renderer.calculate_center(width, height)
1520
-
1521
- @dialog_renderer.draw_floating_window(x, y, width, height, title, content_lines, {
1522
- border_color: "\e[35m", # Magenta
1523
- title_color: "\e[1;35m", # Bold magenta
1524
- content_color: "\e[37m" # White
1525
- })
1526
1544
 
1527
- key = STDIN.getch
1528
- @dialog_renderer.clear_area(x, y, width, height)
1545
+ if screen && renderer
1546
+ # オーバーレイを使用
1547
+ screen.enable_overlay
1548
+ x, y = @dialog_renderer.calculate_center(width, height)
1549
+ @dialog_renderer.draw_floating_window_to_overlay(screen, x, y, width, height, title, content_lines, {
1550
+ border_color: "\e[35m", # Magenta
1551
+ title_color: "\e[1;35m", # Bold magenta
1552
+ content_color: "\e[37m" # White
1553
+ })
1554
+ renderer.render(screen)
1555
+
1556
+ key = STDIN.getch
1557
+ screen.disable_overlay
1558
+ renderer.render(screen)
1559
+ else
1560
+ # フォールバック: 従来の方法
1561
+ x, y = @dialog_renderer.calculate_center(width, height)
1562
+ @dialog_renderer.draw_floating_window(x, y, width, height, title, content_lines, {
1563
+ border_color: "\e[35m", # Magenta
1564
+ title_color: "\e[1;35m", # Bold magenta
1565
+ content_color: "\e[37m" # White
1566
+ })
1567
+
1568
+ key = STDIN.getch
1569
+ @dialog_renderer.clear_area(x, y, width, height)
1570
+ end
1529
1571
 
1530
1572
  case key
1531
1573
  when 'j'
@@ -1571,17 +1613,16 @@ module Rufio
1571
1613
  title = 'Confirm Delete'
1572
1614
  width = 50
1573
1615
  height = content_lines.length + 4
1574
- x, y = @dialog_renderer.calculate_center(width, height)
1575
1616
 
1576
- @dialog_renderer.draw_floating_window(x, y, width, height, title, content_lines, {
1617
+ # オーバーレイダイアログを表示してキー入力を取得
1618
+ key = show_overlay_dialog(title, content_lines, {
1619
+ width: width,
1620
+ height: height,
1577
1621
  border_color: "\e[31m", # Red
1578
1622
  title_color: "\e[1;31m", # Bold red
1579
1623
  content_color: "\e[37m" # White
1580
1624
  })
1581
1625
 
1582
- key = STDIN.getch
1583
- @dialog_renderer.clear_area(x, y, width, height)
1584
-
1585
1626
  key.downcase == 'y'
1586
1627
  end
1587
1628
 
data/lib/rufio/screen.rb CHANGED
@@ -30,6 +30,7 @@ module Rufio
30
30
  @width = width
31
31
  @height = height
32
32
  @cells = Array.new(height) { Array.new(width) { default_cell } }
33
+ @overlay_cells = nil # オーバーレイレイヤー(ダイアログ用)
33
34
  @dirty_rows = Set.new # Phase1: Dirty row tracking
34
35
  end
35
36
 
@@ -116,12 +117,30 @@ module Rufio
116
117
  result = String.new(capacity: @width * 20)
117
118
  current_width = 0 # Phase1: Accumulate width from cells (no recalculation)
118
119
 
119
- @cells[y].each do |cell|
120
- # Skip marker cells for full-width characters
121
- next if cell[:width] == 0
120
+ # オーバーレイがある場合はベース + オーバーレイを合成
121
+ if @overlay_cells
122
+ @width.times do |x|
123
+ overlay_cell = @overlay_cells[y][x]
124
+ base_cell = @cells[y][x]
122
125
 
123
- result << format_cell(cell)
124
- current_width += cell[:width] # Phase1: Use pre-calculated width
126
+ # オーバーレイがあればオーバーレイを使用、なければベースを使用
127
+ cell = overlay_cell || base_cell
128
+
129
+ # マーカーセル(全角文字の2セル目)はスキップ
130
+ next if cell[:width] == 0
131
+
132
+ result << format_cell(cell)
133
+ current_width += cell[:width]
134
+ end
135
+ else
136
+ # オーバーレイなしの場合は従来の処理
137
+ @cells[y].each do |cell|
138
+ # Skip marker cells for full-width characters
139
+ next if cell[:width] == 0
140
+
141
+ result << format_cell(cell)
142
+ current_width += cell[:width] # Phase1: Use pre-calculated width
143
+ end
125
144
  end
126
145
 
127
146
  # Pad the row to full width
@@ -154,6 +173,95 @@ module Rufio
154
173
  @dirty_rows.clear
155
174
  end
156
175
 
176
+ # ===========================================
177
+ # オーバーレイレイヤー(ダイアログ用)
178
+ # ===========================================
179
+
180
+ # オーバーレイレイヤーを有効化
181
+ def enable_overlay
182
+ return if @overlay_cells
183
+
184
+ @overlay_cells = Array.new(@height) { Array.new(@width) { nil } }
185
+ end
186
+
187
+ # オーバーレイレイヤーを無効化してクリア
188
+ def disable_overlay
189
+ return unless @overlay_cells
190
+
191
+ # オーバーレイが描画されていた行をdirtyにマーク
192
+ @height.times do |y|
193
+ if @overlay_cells[y].any? { |cell| cell }
194
+ @dirty_rows.add(y)
195
+ end
196
+ end
197
+
198
+ @overlay_cells = nil
199
+ end
200
+
201
+ # オーバーレイレイヤーをクリア
202
+ def clear_overlay
203
+ return unless @overlay_cells
204
+
205
+ @height.times do |y|
206
+ if @overlay_cells[y].any? { |cell| cell }
207
+ @dirty_rows.add(y)
208
+ @overlay_cells[y] = Array.new(@width) { nil }
209
+ end
210
+ end
211
+ end
212
+
213
+ # オーバーレイレイヤーが有効かどうか
214
+ def overlay_enabled?
215
+ !@overlay_cells.nil?
216
+ end
217
+
218
+ # オーバーレイレイヤーに描画
219
+ def put_overlay(x, y, char, fg: nil, bg: nil, width: nil)
220
+ return unless @overlay_cells
221
+ return if out_of_bounds?(x, y)
222
+
223
+ char_width = width || TextUtils.display_width(char)
224
+ @overlay_cells[y][x] = {
225
+ char: char,
226
+ fg: fg,
227
+ bg: bg,
228
+ width: char_width
229
+ }
230
+
231
+ @dirty_rows.add(y)
232
+
233
+ # 全角文字の場合、次のセルをマーク
234
+ if char_width >= 2 && x + 1 < @width
235
+ (char_width - 1).times do |offset|
236
+ next_x = x + 1 + offset
237
+ break if next_x >= @width
238
+ @overlay_cells[y][next_x] = {
239
+ char: '',
240
+ fg: nil,
241
+ bg: nil,
242
+ width: 0
243
+ }
244
+ end
245
+ end
246
+ end
247
+
248
+ # オーバーレイレイヤーに文字列を描画
249
+ def put_overlay_string(x, y, str, fg: nil, bg: nil)
250
+ return unless @overlay_cells
251
+ return if out_of_bounds?(x, y)
252
+
253
+ clean_str = str.include?("\e") ? ColorHelper.strip_ansi(str) : str
254
+
255
+ current_x = x
256
+ clean_str.each_char do |char|
257
+ break if current_x >= @width
258
+
259
+ char_width = TextUtils.display_width(char)
260
+ put_overlay(current_x, y, char, fg: fg, bg: bg, width: char_width)
261
+ current_x += char_width
262
+ end
263
+ end
264
+
157
265
  private
158
266
 
159
267
  def default_cell