rufio 0.63.0 → 0.64.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 903b51d0ee611d29ad7ea8961e7948b9dceae16b6d04280c28252149ae2fdd20
4
- data.tar.gz: 287941ec941e8200c98a881c3ce6de2f7cc7ce3b13e63ba17875ff2222c92bc8
3
+ metadata.gz: a25103ee7f5bae19e38110e69a2eeb2ca66eb0b781dba23d271ceafb22933087
4
+ data.tar.gz: ecc2574678d47565dbc8ad0f0797c7b59a13a2e04cf70f5a5ee21013e089ef98
5
5
  SHA512:
6
- metadata.gz: ab00eea888ea729262a4b8c787ecdf43814afe0c573fdc2624597b014027836ae81da4342b413024a10ba6f3212faf5cee1f987af45357fc99134f93d3425314
7
- data.tar.gz: c665a171a03ab2ebe1941d7b38eeac86ccbd1552c3ee5a7e8cf252fa20293470399cf4cf1ecdeacba8d0d6fb251d5cd4b87ae74a4b4e924814d6821c34d99857
6
+ metadata.gz: 9e9e60071f2ac2ef3c6ac141a6ae04dd38b0fd9545655f2e24f2a42f14ca8767c905a8cd556898b4c9aee629c0e6153ee8a03f7bae4c3bc5d3a6bbc644a6c3d0
7
+ data.tar.gz: 3b0be66f3ecd1d6d65152375e3ae1659ee8f0a4bf90e87475e3fed088a3427d3ab09143b0f2287dd642e8b47737fbc99787e7f26a6b8ff3710da5eeed27d1432
data/CHANGELOG.md CHANGED
@@ -7,6 +7,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.64.0] - 2026-02-07
11
+
12
+ ### Added
13
+ - **Screen Overlay Layer**: ダイアログ描画用のオーバーレイレイヤーをScreenクラスに追加
14
+ - `enable_overlay` / `disable_overlay` / `clear_overlay` / `overlay_enabled?`
15
+ - `put_overlay` / `put_overlay_string` でオーバーレイレイヤーに描画
16
+ - `row()` メソッドがオーバーレイとベースレイヤーを自動合成
17
+ - オーバーレイ無効化時にdirty行を自動マーク(再描画保証)
18
+ - **`show_overlay_dialog` ヘルパーメソッド**: TerminalUI, KeybindHandler, BookmarkManager, CommandModeUI, ZoxideIntegration に統一的なオーバーレイダイアログ表示メソッドを追加
19
+ - `terminal_ui` が利用可能な場合はオーバーレイを使用、なければ従来の直接描画にフォールバック
20
+ - **`draw_floating_window_to_overlay`**: DialogRendererにオーバーレイレイヤーへのフローティングウィンドウ描画メソッドを追加
21
+ - **Screen Overlay テスト**: `test/test_screen_overlay.rb` を追加
22
+
23
+ ### Changed
24
+ - **ダイアログ描画のバッファベース化**: 全ダイアログ表示をScreenオーバーレイ経由に変更
25
+ - BookmarkManager: ブックマークメニュー、一覧、リネーム、削除、確認ダイアログ
26
+ - KeybindHandler: 削除確認、コピー/移動確認、終了確認、スクリプトパス管理、ブックマーク操作結果
27
+ - CommandModeUI: コマンド実行結果表示
28
+ - ZoxideIntegration: 履歴なしメッセージ、履歴選択ダイアログ
29
+ - TerminalUI: ヘルプダイアログ、お知らせ表示、プラグイン読み込みエラー
30
+ - **コマンドモードのオーバーレイ化**: 直接描画(Screenバッファ外)からオーバーレイベースのバッファ描画に変更
31
+ - `draw_command_mode_to_overlay` メソッドでメインループ内のバッファ描画に統合
32
+ - **Box Drawing文字の幅修正**: `TextUtils.char_width` で罫線文字(U+2500-U+257F)を幅1として扱うように修正(ターミナルでの実際の表示幅に合わせた)
33
+ - **コードスタイル統一**: `lib/rufio.rb` の `require_relative` をダブルクォートからシングルクォートに統一
34
+ - **`set_terminal_ui` の伝播**: KeybindHandler経由でBookmarkManager, ZoxideIntegrationにも `terminal_ui` を設定
35
+ - **TerminalUI**: `screen` と `renderer` の `attr_reader` を公開
36
+
37
+ ### Technical Details
38
+ - **新規ファイル**: `test/test_screen_overlay.rb`
39
+ - **変更ファイル**: `lib/rufio.rb`, `lib/rufio/screen.rb`, `lib/rufio/dialog_renderer.rb`, `lib/rufio/terminal_ui.rb`, `lib/rufio/keybind_handler.rb`, `lib/rufio/bookmark_manager.rb`, `lib/rufio/command_mode_ui.rb`, `lib/rufio/text_utils.rb`, `lib/rufio/zoxide_integration.rb`
40
+
10
41
  ## [0.63.0] - 2026-02-01
11
42
 
12
43
  ### Added
@@ -10,6 +10,12 @@ module Rufio
10
10
  def initialize(bookmark = nil, dialog_renderer = nil)
11
11
  @bookmark = bookmark || create_default_bookmark
12
12
  @dialog_renderer = dialog_renderer
13
+ @terminal_ui = nil
14
+ end
15
+
16
+ # terminal_ui を設定
17
+ def set_terminal_ui(terminal_ui)
18
+ @terminal_ui = terminal_ui
13
19
  end
14
20
 
15
21
  private
@@ -46,40 +52,45 @@ module Rufio
46
52
 
47
53
  dialog_width = 45
48
54
  dialog_height = 4 + content_lines.length
49
- x, y = @dialog_renderer.calculate_center(dialog_width, dialog_height)
50
-
51
- @dialog_renderer.draw_floating_window(x, y, dialog_width, dialog_height, title, content_lines, {
52
- border_color: "\e[34m", # Blue
53
- title_color: "\e[1;34m", # Bold blue
54
- content_color: "\e[37m" # White
55
- })
56
-
57
- # Wait for key input
58
- loop do
59
- input = STDIN.getch.downcase
60
-
61
- case input
62
- when 'a'
63
- @dialog_renderer.clear_area(x, y, dialog_width, dialog_height)
64
- return { action: :add, path: current_path }
65
- when 'l'
66
- @dialog_renderer.clear_area(x, y, dialog_width, dialog_height)
67
- return { action: :list }
68
- when 'r'
69
- @dialog_renderer.clear_area(x, y, dialog_width, dialog_height)
70
- return { action: :rename }
71
- when 'd'
72
- @dialog_renderer.clear_area(x, y, dialog_width, dialog_height)
73
- return { action: :remove }
74
- when '1', '2', '3', '4', '5', '6', '7', '8', '9'
75
- @dialog_renderer.clear_area(x, y, dialog_width, dialog_height)
76
- return { action: :navigate, number: input.to_i }
77
- else
78
- # Cancel
79
- @dialog_renderer.clear_area(x, y, dialog_width, dialog_height)
80
- return { action: :cancel }
55
+
56
+ result = { action: :cancel }
57
+ show_overlay_dialog(title, content_lines, {
58
+ width: dialog_width,
59
+ height: dialog_height,
60
+ border_color: "\e[34m", # Blue
61
+ title_color: "\e[1;34m", # Bold blue
62
+ content_color: "\e[37m" # White
63
+ }) do
64
+ # Wait for key input
65
+ loop do
66
+ input = STDIN.getch.downcase
67
+
68
+ case input
69
+ when 'a'
70
+ result = { action: :add, path: current_path }
71
+ break
72
+ when 'l'
73
+ result = { action: :list }
74
+ break
75
+ when 'r'
76
+ result = { action: :rename }
77
+ break
78
+ when 'd'
79
+ result = { action: :remove }
80
+ break
81
+ when '1', '2', '3', '4', '5', '6', '7', '8', '9'
82
+ result = { action: :navigate, number: input.to_i }
83
+ break
84
+ else
85
+ # Cancel
86
+ result = { action: :cancel }
87
+ break
88
+ end
81
89
  end
90
+ nil
82
91
  end
92
+
93
+ result
83
94
  end
84
95
 
85
96
  # Add a bookmark interactively
@@ -141,32 +152,33 @@ module Rufio
141
152
  title = 'Rename Bookmark'
142
153
  width = 60
143
154
  height = [content_lines.length + 4, 20].min
144
- x, y = @dialog_renderer.calculate_center(width, height)
145
155
 
146
- @dialog_renderer.draw_floating_window(x, y, width, height, title, content_lines, {
156
+ # Wait for selection
157
+ selected_number = nil
158
+ show_overlay_dialog(title, content_lines, {
159
+ width: width,
160
+ height: height,
147
161
  border_color: "\e[33m", # Yellow
148
162
  title_color: "\e[1;33m", # Bold yellow
149
163
  content_color: "\e[37m" # White
150
- })
151
-
152
- # Wait for selection
153
- selected_number = nil
154
- loop do
155
- input = STDIN.getch.downcase
156
-
157
- if input >= '1' && input <= '9'
158
- number = input.to_i
159
- if number > 0 && number <= bookmarks.length
160
- selected_number = number
164
+ }) do
165
+ loop do
166
+ input = STDIN.getch.downcase
167
+
168
+ if input >= '1' && input <= '9'
169
+ number = input.to_i
170
+ if number > 0 && number <= bookmarks.length
171
+ selected_number = number
172
+ break
173
+ end
174
+ elsif input == "\e" # ESC
161
175
  break
162
176
  end
163
- elsif input == "\e" # ESC
164
- @dialog_renderer.clear_area(x, y, width, height)
165
- return false
166
177
  end
178
+ nil
167
179
  end
168
180
 
169
- @dialog_renderer.clear_area(x, y, width, height)
181
+ return false unless selected_number
170
182
 
171
183
  # Get new name
172
184
  bookmark_to_rename = bookmarks[selected_number - 1]
@@ -216,32 +228,33 @@ module Rufio
216
228
  title = 'Delete Bookmark'
217
229
  width = 60
218
230
  height = [content_lines.length + 4, 20].min
219
- x, y = @dialog_renderer.calculate_center(width, height)
220
231
 
221
- @dialog_renderer.draw_floating_window(x, y, width, height, title, content_lines, {
232
+ # Wait for selection
233
+ selected_number = nil
234
+ show_overlay_dialog(title, content_lines, {
235
+ width: width,
236
+ height: height,
222
237
  border_color: "\e[31m", # Red (warning)
223
238
  title_color: "\e[1;31m", # Bold red
224
239
  content_color: "\e[37m" # White
225
- })
226
-
227
- # Wait for selection
228
- selected_number = nil
229
- loop do
230
- input = STDIN.getch.downcase
231
-
232
- if input >= '1' && input <= '9'
233
- number = input.to_i
234
- if number > 0 && number <= bookmarks.length
235
- selected_number = number
240
+ }) do
241
+ loop do
242
+ input = STDIN.getch.downcase
243
+
244
+ if input >= '1' && input <= '9'
245
+ number = input.to_i
246
+ if number > 0 && number <= bookmarks.length
247
+ selected_number = number
248
+ break
249
+ end
250
+ elsif input == "\e" # ESC
236
251
  break
237
252
  end
238
- elsif input == "\e" # ESC
239
- @dialog_renderer.clear_area(x, y, width, height)
240
- return false
241
253
  end
254
+ nil
242
255
  end
243
256
 
244
- @dialog_renderer.clear_area(x, y, width, height)
257
+ return false unless selected_number
245
258
 
246
259
  # Confirm deletion
247
260
  bookmark_to_remove = bookmarks[selected_number - 1]
@@ -284,31 +297,32 @@ module Rufio
284
297
  title = 'Bookmarks'
285
298
  width = 60
286
299
  height = [content_lines.length + 4, 20].min
287
- x, y = @dialog_renderer.calculate_center(width, height)
288
300
 
289
- @dialog_renderer.draw_floating_window(x, y, width, height, title, content_lines, {
301
+ # Wait for selection
302
+ selected_bookmark = nil
303
+ show_overlay_dialog(title, content_lines, {
304
+ width: width,
305
+ height: height,
290
306
  border_color: "\e[34m", # Blue
291
307
  title_color: "\e[1;34m", # Bold blue
292
308
  content_color: "\e[37m" # White
293
- })
294
-
295
- # Wait for selection
296
- selected_bookmark = nil
297
- loop do
298
- input = STDIN.getch.downcase
299
-
300
- if input >= '1' && input <= '9'
301
- number = input.to_i
302
- if number > 0 && number <= bookmarks.length
303
- selected_bookmark = bookmarks[number - 1]
309
+ }) do
310
+ loop do
311
+ input = STDIN.getch.downcase
312
+
313
+ if input >= '1' && input <= '9'
314
+ number = input.to_i
315
+ if number > 0 && number <= bookmarks.length
316
+ selected_bookmark = bookmarks[number - 1]
317
+ break
318
+ end
319
+ elsif input == "\e" # ESC
304
320
  break
305
321
  end
306
- elsif input == "\e" # ESC
307
- break
308
322
  end
323
+ nil
309
324
  end
310
325
 
311
- @dialog_renderer.clear_area(x, y, width, height)
312
326
  selected_bookmark
313
327
  end
314
328
 
@@ -376,6 +390,48 @@ module Rufio
376
390
 
377
391
  private
378
392
 
393
+ # オーバーレイダイアログを表示してキー入力を待つヘルパーメソッド
394
+ def show_overlay_dialog(title, content_lines, options = {}, &block)
395
+ # terminal_ui が利用可能で、screen と renderer が存在する場合のみオーバーレイを使用
396
+ use_overlay = @terminal_ui &&
397
+ @terminal_ui.respond_to?(:screen) &&
398
+ @terminal_ui.respond_to?(:renderer) &&
399
+ @terminal_ui.screen &&
400
+ @terminal_ui.renderer
401
+
402
+ if use_overlay
403
+ # オーバーレイを使用
404
+ @terminal_ui.show_overlay_dialog(title, content_lines, options, &block)
405
+ else
406
+ # フォールバック: 従来の方法
407
+ width = options[:width]
408
+ height = options[:height]
409
+
410
+ unless width && height
411
+ width, height = @dialog_renderer.calculate_dimensions(content_lines, {
412
+ title: title,
413
+ min_width: options[:min_width] || 40,
414
+ max_width: options[:max_width] || 80
415
+ })
416
+ end
417
+
418
+ x, y = @dialog_renderer.calculate_center(width, height)
419
+
420
+ @dialog_renderer.draw_floating_window(x, y, width, height, title, content_lines, {
421
+ border_color: options[:border_color] || "\e[37m",
422
+ title_color: options[:title_color] || "\e[1;33m",
423
+ content_color: options[:content_color] || "\e[37m"
424
+ })
425
+
426
+ key = block_given? ? yield : STDIN.getch
427
+
428
+ @dialog_renderer.clear_area(x, y, width, height)
429
+ @terminal_ui&.refresh_display
430
+
431
+ key
432
+ end
433
+ end
434
+
379
435
  # Show result dialog with success or error styling
380
436
  # @param title [String] Dialog title
381
437
  # @param message [String] Result message
@@ -401,16 +457,15 @@ module Rufio
401
457
 
402
458
  width = 50
403
459
  height = 6
404
- x, y = @dialog_renderer.calculate_center(width, height)
405
460
 
406
- @dialog_renderer.draw_floating_window(x, y, width, height, title, content_lines, {
461
+ # オーバーレイダイアログを表示
462
+ show_overlay_dialog(title, content_lines, {
463
+ width: width,
464
+ height: height,
407
465
  border_color: border_color,
408
466
  title_color: title_color,
409
467
  content_color: "\e[37m"
410
468
  })
411
-
412
- STDIN.getch
413
- @dialog_renderer.clear_area(x, y, width, height)
414
469
  end
415
470
 
416
471
  # Show confirmation dialog for bookmark removal
@@ -431,30 +486,31 @@ module Rufio
431
486
  title = 'Confirm Delete'
432
487
  width = 50
433
488
  height = content_lines.length + 4
434
- x, y = @dialog_renderer.calculate_center(width, height)
435
489
 
436
- @dialog_renderer.draw_floating_window(x, y, width, height, title, content_lines, {
490
+ # Wait for confirmation
491
+ confirmed = false
492
+ show_overlay_dialog(title, content_lines, {
493
+ width: width,
494
+ height: height,
437
495
  border_color: "\e[33m", # Yellow (warning)
438
496
  title_color: "\e[1;33m", # Bold yellow
439
497
  content_color: "\e[37m" # White
440
- })
498
+ }) do
499
+ loop do
500
+ input = STDIN.getch.downcase
441
501
 
442
- # Wait for confirmation
443
- confirmed = false
444
- loop do
445
- input = STDIN.getch.downcase
446
-
447
- case input
448
- when 'y'
449
- confirmed = true
450
- break
451
- when 'n', "\e" # n or ESC
452
- confirmed = false
453
- break
502
+ case input
503
+ when 'y'
504
+ confirmed = true
505
+ break
506
+ when 'n', "\e" # n or ESC
507
+ confirmed = false
508
+ break
509
+ end
454
510
  end
511
+ nil
455
512
  end
456
513
 
457
- @dialog_renderer.clear_area(x, y, width, height)
458
514
  confirmed
459
515
  end
460
516
  end
@@ -8,10 +8,16 @@ module Rufio
8
8
  def initialize(command_mode, dialog_renderer)
9
9
  @command_mode = command_mode
10
10
  @dialog_renderer = dialog_renderer
11
+ @terminal_ui = nil
11
12
  # 最後に表示したウィンドウの位置とサイズを保存
12
13
  @last_window = nil
13
14
  end
14
15
 
16
+ # terminal_ui を設定
17
+ def set_terminal_ui(terminal_ui)
18
+ @terminal_ui = terminal_ui
19
+ end
20
+
15
21
  # 入力文字列に対する補完候補を取得
16
22
  # @param input [String] 現在の入力文字列
17
23
  # @return [Array<String>] 補完候補の配列
@@ -124,21 +130,14 @@ module Rufio
124
130
  max_width: 100
125
131
  })
126
132
 
127
- # 中央位置を計算
128
- x, y = @dialog_renderer.calculate_center(width, height)
129
-
130
- # フローティングウィンドウを描画
131
- @dialog_renderer.draw_floating_window(x, y, width, height, title, content_lines, {
132
- border_color: border_color,
133
- title_color: title_color,
134
- content_color: content_color
135
- })
136
-
137
- # キー入力を待つ
138
- STDIN.getch
139
-
140
- # ウィンドウをクリア
141
- @dialog_renderer.clear_area(x, y, width, height)
133
+ # オーバーレイダイアログを表示
134
+ show_overlay_dialog(title, content_lines, {
135
+ width: width,
136
+ height: height,
137
+ border_color: border_color,
138
+ title_color: title_color,
139
+ content_color: content_color
140
+ })
142
141
  end
143
142
 
144
143
  # コマンド入力プロンプトをクリア
@@ -156,6 +155,48 @@ module Rufio
156
155
 
157
156
  private
158
157
 
158
+ # オーバーレイダイアログを表示してキー入力を待つヘルパーメソッド
159
+ def show_overlay_dialog(title, content_lines, options = {}, &block)
160
+ # terminal_ui が利用可能で、screen と renderer が存在する場合のみオーバーレイを使用
161
+ use_overlay = @terminal_ui &&
162
+ @terminal_ui.respond_to?(:screen) &&
163
+ @terminal_ui.respond_to?(:renderer) &&
164
+ @terminal_ui.screen &&
165
+ @terminal_ui.renderer
166
+
167
+ if use_overlay
168
+ # オーバーレイを使用
169
+ @terminal_ui.show_overlay_dialog(title, content_lines, options, &block)
170
+ else
171
+ # フォールバック: 従来の方法
172
+ width = options[:width]
173
+ height = options[:height]
174
+
175
+ unless width && height
176
+ width, height = @dialog_renderer.calculate_dimensions(content_lines, {
177
+ title: title,
178
+ min_width: options[:min_width] || 40,
179
+ max_width: options[:max_width] || 80
180
+ })
181
+ end
182
+
183
+ x, y = @dialog_renderer.calculate_center(width, height)
184
+
185
+ @dialog_renderer.draw_floating_window(x, y, width, height, title, content_lines, {
186
+ border_color: options[:border_color] || "\e[37m",
187
+ title_color: options[:title_color] || "\e[1;33m",
188
+ content_color: options[:content_color] || "\e[37m"
189
+ })
190
+
191
+ key = block_given? ? yield : STDIN.getch
192
+
193
+ @dialog_renderer.clear_area(x, y, width, height)
194
+ @terminal_ui&.refresh_display
195
+
196
+ key
197
+ end
198
+ end
199
+
159
200
  # 文字列配列の共通プレフィックスを見つける
160
201
  # @param strings [Array<String>] 文字列配列
161
202
  # @return [String] 共通プレフィックス
@@ -75,6 +75,73 @@ module Rufio
75
75
  screen.put_string(x, bottom_y, "└#{'─' * (width - 2)}┘", fg: border_color)
76
76
  end
77
77
 
78
+ # オーバーレイレイヤーにフローティングウィンドウを描画
79
+ # @param screen [Screen] Screen buffer with overlay enabled
80
+ # @param x [Integer] X position (column)
81
+ # @param y [Integer] Y position (row)
82
+ # @param width [Integer] Window width
83
+ # @param height [Integer] Window height
84
+ # @param title [String, nil] Window title (optional)
85
+ # @param content_lines [Array<String>] Content lines to display
86
+ # @param options [Hash] Customization options
87
+ def draw_floating_window_to_overlay(screen, x, y, width, height, title, content_lines, options = {})
88
+ # オーバーレイが有効でない場合は有効化
89
+ screen.enable_overlay unless screen.overlay_enabled?
90
+
91
+ border_color = options[:border_color] || "\e[37m"
92
+ title_color = options[:title_color] || "\e[1;33m"
93
+ content_color = options[:content_color] || "\e[37m"
94
+
95
+ # Draw top border
96
+ screen.put_overlay_string(x, y, "┌#{'─' * (width - 2)}┐", fg: border_color)
97
+
98
+ # Draw title line if title exists
99
+ if title
100
+ title_width = TextUtils.display_width(title)
101
+ title_padding = (width - 2 - title_width) / 2
102
+ padded_title = ' ' * title_padding + title
103
+ title_line = TextUtils.pad_string_to_width(padded_title, width - 2)
104
+
105
+ screen.put_overlay(x, y + 1, '│', fg: border_color)
106
+ screen.put_overlay_string(x + 1, y + 1, title_line, fg: title_color)
107
+ screen.put_overlay(x + width - 1, y + 1, '│', fg: border_color)
108
+
109
+ # Draw title separator
110
+ screen.put_overlay_string(x, y + 2, "├#{'─' * (width - 2)}┤", fg: border_color)
111
+ content_start_y = y + 3
112
+ else
113
+ content_start_y = y + 1
114
+ end
115
+
116
+ # Draw content lines
117
+ content_height = title ? height - 4 : height - 2
118
+ content_lines.each_with_index do |line, index|
119
+ break if index >= content_height
120
+
121
+ line_y = content_start_y + index
122
+ line_content = TextUtils.pad_string_to_width(line, width - 2)
123
+
124
+ screen.put_overlay(x, line_y, '│', fg: border_color)
125
+ screen.put_overlay_string(x + 1, line_y, line_content, fg: content_color)
126
+ screen.put_overlay(x + width - 1, line_y, '│', fg: border_color)
127
+ end
128
+
129
+ # Fill remaining lines with empty space
130
+ remaining_lines = content_height - content_lines.length
131
+ remaining_lines.times do |i|
132
+ line_y = content_start_y + content_lines.length + i
133
+ empty_line = ' ' * (width - 2)
134
+
135
+ screen.put_overlay(x, line_y, '│', fg: border_color)
136
+ screen.put_overlay_string(x + 1, line_y, empty_line)
137
+ screen.put_overlay(x + width - 1, line_y, '│', fg: border_color)
138
+ end
139
+
140
+ # Draw bottom border
141
+ bottom_y = y + height - 1
142
+ screen.put_overlay_string(x, bottom_y, "└#{'─' * (width - 2)}┘", fg: border_color)
143
+ end
144
+
78
145
  # Draw a floating window with title, content, and customizable colors
79
146
  # @param x [Integer] X position (column)
80
147
  # @param y [Integer] Y position (row)