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.
@@ -8,6 +8,8 @@ module Rufio
8
8
  def initialize(command_mode, dialog_renderer)
9
9
  @command_mode = command_mode
10
10
  @dialog_renderer = dialog_renderer
11
+ # 最後に表示したウィンドウの位置とサイズを保存
12
+ @last_window = nil
11
13
  end
12
14
 
13
15
  # 入力文字列に対する補完候補を取得
@@ -68,6 +70,9 @@ module Rufio
68
70
  # 中央位置を計算
69
71
  x, y = @dialog_renderer.calculate_center(width, height)
70
72
 
73
+ # ウィンドウの位置とサイズを保存
74
+ @last_window = { x: x, y: y, width: width, height: height }
75
+
71
76
  # フローティングウィンドウを描画
72
77
  @dialog_renderer.draw_floating_window(x, y, width, height, title, content_lines, {
73
78
  border_color: border_color,
@@ -136,6 +141,19 @@ module Rufio
136
141
  @dialog_renderer.clear_area(x, y, width, height)
137
142
  end
138
143
 
144
+ # コマンド入力プロンプトをクリア
145
+ def clear_prompt
146
+ return unless @last_window
147
+
148
+ @dialog_renderer.clear_area(
149
+ @last_window[:x],
150
+ @last_window[:y],
151
+ @last_window[:width],
152
+ @last_window[:height]
153
+ )
154
+ @last_window = nil
155
+ end
156
+
139
157
  private
140
158
 
141
159
  # 文字列配列の共通プレフィックスを見つける
@@ -7,6 +7,74 @@ module Rufio
7
7
  class DialogRenderer
8
8
  include TextUtils
9
9
 
10
+ # Phase 4: Screenバッファにフローティングウィンドウを描画
11
+ # @param screen [Screen] Screen buffer to draw to
12
+ # @param x [Integer] X position (column)
13
+ # @param y [Integer] Y position (row)
14
+ # @param width [Integer] Window width
15
+ # @param height [Integer] Window height
16
+ # @param title [String, nil] Window title (optional)
17
+ # @param content_lines [Array<String>] Content lines to display
18
+ # @param options [Hash] Customization options
19
+ # @option options [String] :border_color Border color ANSI code
20
+ # @option options [String] :title_color Title color ANSI code
21
+ # @option options [String] :content_color Content color ANSI code
22
+ def draw_floating_window_to_buffer(screen, x, y, width, height, title, content_lines, options = {})
23
+ # Default options
24
+ border_color = options[:border_color] || "\e[37m" # White
25
+ title_color = options[:title_color] || "\e[1;33m" # Bold yellow
26
+ content_color = options[:content_color] || "\e[37m" # White
27
+
28
+ # Draw top border
29
+ screen.put_string(x, y, "┌#{'─' * (width - 2)}┐", fg: border_color)
30
+
31
+ # Draw title line if title exists
32
+ if title
33
+ title_width = TextUtils.display_width(title)
34
+ title_padding = (width - 2 - title_width) / 2
35
+ padded_title = ' ' * title_padding + title
36
+ title_line = TextUtils.pad_string_to_width(padded_title, width - 2)
37
+
38
+ screen.put(x, y + 1, '│', fg: border_color)
39
+ screen.put_string(x + 1, y + 1, title_line, fg: title_color)
40
+ screen.put(x + width - 1, y + 1, '│', fg: border_color)
41
+
42
+ # Draw title separator
43
+ screen.put_string(x, y + 2, "├#{'─' * (width - 2)}┤", fg: border_color)
44
+ content_start_y = y + 3
45
+ else
46
+ content_start_y = y + 1
47
+ end
48
+
49
+ # Draw content lines
50
+ content_height = title ? height - 4 : height - 2
51
+ content_lines.each_with_index do |line, index|
52
+ break if index >= content_height
53
+
54
+ line_y = content_start_y + index
55
+ line_content = TextUtils.pad_string_to_width(line, width - 2)
56
+
57
+ screen.put(x, line_y, '│', fg: border_color)
58
+ screen.put_string(x + 1, line_y, line_content, fg: content_color)
59
+ screen.put(x + width - 1, line_y, '│', fg: border_color)
60
+ end
61
+
62
+ # Fill remaining lines with empty space
63
+ remaining_lines = content_height - content_lines.length
64
+ remaining_lines.times do |i|
65
+ line_y = content_start_y + content_lines.length + i
66
+ empty_line = ' ' * (width - 2)
67
+
68
+ screen.put(x, line_y, '│', fg: border_color)
69
+ screen.put_string(x + 1, line_y, empty_line)
70
+ screen.put(x + width - 1, line_y, '│', fg: border_color)
71
+ end
72
+
73
+ # Draw bottom border
74
+ bottom_y = y + height - 1
75
+ screen.put_string(x, bottom_y, "└#{'─' * (width - 2)}┘", fg: border_color)
76
+ end
77
+
10
78
  # Draw a floating window with title, content, and customizable colors
11
79
  # @param x [Integer] X position (column)
12
80
  # @param y [Integer] Y position (row)
@@ -545,7 +545,7 @@ module Rufio
545
545
  end
546
546
 
547
547
  def exit_request
548
- true # request exit
548
+ show_exit_confirmation
549
549
  end
550
550
 
551
551
  def fzf_search
@@ -1117,6 +1117,53 @@ module Rufio
1117
1117
  end
1118
1118
  end
1119
1119
 
1120
+ def show_exit_confirmation
1121
+ # コンテンツの準備
1122
+ title = 'Exit Confirmation'
1123
+
1124
+ content_lines = [
1125
+ '',
1126
+ 'Are you sure you want to exit?',
1127
+ '',
1128
+ ' [Y]es - Exit',
1129
+ ' [N]o - Cancel',
1130
+ ''
1131
+ ]
1132
+
1133
+ # ダイアログのサイズ設定
1134
+ dialog_width = CONFIRMATION_DIALOG_WIDTH
1135
+ dialog_height = DIALOG_BORDER_HEIGHT + content_lines.length
1136
+
1137
+ # ダイアログの位置を中央に設定
1138
+ x, y = @dialog_renderer.calculate_center(dialog_width, dialog_height)
1139
+
1140
+ # ダイアログの描画(終了は黄色で表示)
1141
+ @dialog_renderer.draw_floating_window(x, y, dialog_width, dialog_height, title, content_lines, {
1142
+ border_color: "\e[33m", # 黄色(注意)
1143
+ title_color: "\e[1;33m", # 太字黄色
1144
+ content_color: "\e[37m" # 白色
1145
+ })
1146
+
1147
+ # キー入力待機
1148
+ loop do
1149
+ input = STDIN.getch.downcase
1150
+
1151
+ case input
1152
+ when 'y'
1153
+ # ダイアログをクリア
1154
+ @dialog_renderer.clear_area(x, y, dialog_width, dialog_height)
1155
+ @terminal_ui&.refresh_display # 画面を再描画
1156
+ return true
1157
+ when 'n', "\e", "\x03" # n, ESC, Ctrl+C
1158
+ # ダイアログをクリア
1159
+ @dialog_renderer.clear_area(x, y, dialog_width, dialog_height)
1160
+ @terminal_ui&.refresh_display # 画面を再描画
1161
+ return false
1162
+ end
1163
+ # 無効なキー入力の場合は再度ループ
1164
+ end
1165
+ end
1166
+
1120
1167
  # パスを指定した長さに短縮
1121
1168
  def shorten_path(path, max_length)
1122
1169
  return path if path.length <= max_length
@@ -1455,7 +1502,7 @@ module Rufio
1455
1502
 
1456
1503
  # プロジェクトモード中のキー処理
1457
1504
  def handle_project_mode_key(key)
1458
- case key
1505
+ result = case key
1459
1506
  when "\e" # ESC - ログモードならプロジェクトモードに戻る、そうでなければ終了
1460
1507
  if @in_log_mode
1461
1508
  exit_log_mode
@@ -1507,6 +1554,10 @@ module Rufio
1507
1554
  else
1508
1555
  false
1509
1556
  end
1557
+
1558
+ # キー処理後、プロジェクトモードの再描画をトリガー
1559
+ @terminal_ui&.trigger_project_mode_redraw if result && @in_project_mode
1560
+ result
1510
1561
  end
1511
1562
 
1512
1563
  # プロジェクトモード用のエントリ数取得
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rufio
4
+ module Plugins
5
+ # Hello コマンドを提供するプラグイン
6
+ # Rubyコードで挨拶を返す簡単な例
7
+ class Stop < Plugin
8
+ def name
9
+ 'Stop'
10
+ end
11
+
12
+ def description
13
+ 'Rubyで実装された挨拶コマンドの例'
14
+ end
15
+
16
+ def commands
17
+ {
18
+ stop: method(:say_hello)
19
+ }
20
+ end
21
+
22
+ private
23
+
24
+ # 挨拶メッセージを返す
25
+ def say_hello
26
+ 'stop 5seconds'
27
+ sleep 5
28
+ 'done'
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rufio
4
+ # Renderer class - Front buffer for double buffering
5
+ #
6
+ # Manages the front buffer (what's currently displayed on screen)
7
+ # and performs differential rendering by comparing with the back buffer (Screen).
8
+ #
9
+ # Features:
10
+ # - Diff rendering: Only updates changed lines
11
+ # - Cursor positioning: Uses ANSI escape codes
12
+ # - Flush control: Ensures all output is displayed
13
+ #
14
+ class Renderer
15
+ def initialize(width, height, output: STDOUT)
16
+ @width = width
17
+ @height = height
18
+ @front = Array.new(height) { " " * width }
19
+ @output = output
20
+ end
21
+
22
+ # Render the screen with differential updates
23
+ #
24
+ # @param screen [Screen] The back buffer to render
25
+ def render(screen)
26
+ # Phase1: Only process dirty rows (rows that have changed)
27
+ screen.dirty_rows.each do |y|
28
+ line = screen.row(y)
29
+ next if line == @front[y] # Skip if content is actually the same
30
+
31
+ # Move cursor to line y (1-indexed) and output the line
32
+ @output.print "\e[#{y + 1};1H#{line}"
33
+ @front[y] = line
34
+ end
35
+
36
+ # Phase1: Clear dirty tracking after rendering
37
+ screen.clear_dirty
38
+ @output.flush
39
+ end
40
+
41
+ # Resize the front buffer
42
+ #
43
+ # @param width [Integer] New width
44
+ # @param height [Integer] New height
45
+ def resize(width, height)
46
+ @width = width
47
+ @height = height
48
+ @front = Array.new(height) { " " * width }
49
+
50
+ # Clear entire screen
51
+ @output.print "\e[2J\e[H"
52
+ @output.flush
53
+ end
54
+
55
+ # Clear the front buffer and screen
56
+ def clear
57
+ @front = Array.new(@height) { " " * @width }
58
+
59
+ # Clear screen and move cursor to home
60
+ @output.print "\e[2J\e[H"
61
+ @output.flush
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ module Rufio
6
+ # Screen class - Back buffer for double buffering
7
+ #
8
+ # Manages a virtual screen buffer where each cell contains:
9
+ # - Character
10
+ # - Foreground color (ANSI code)
11
+ # - Background color (ANSI code)
12
+ # - Display width (for multibyte characters)
13
+ #
14
+ # Supports:
15
+ # - ASCII characters (width = 1)
16
+ # - Full-width characters (width = 2, e.g., Japanese, Chinese)
17
+ # - Emoji (width = 2+)
18
+ #
19
+ # Phase1 Optimizations:
20
+ # - Width pre-calculation (computed once in put method)
21
+ # - Dirty row tracking (only render changed rows)
22
+ # - Optimized format_cell (String.new with capacity)
23
+ # - Optimized row generation (width accumulation, no ANSI strip)
24
+ # - Minimal ANSI stripping (only once in put_string)
25
+ #
26
+ class Screen
27
+ attr_reader :width, :height
28
+
29
+ def initialize(width, height)
30
+ @width = width
31
+ @height = height
32
+ @cells = Array.new(height) { Array.new(width) { default_cell } }
33
+ @dirty_rows = Set.new # Phase1: Dirty row tracking
34
+ end
35
+
36
+ # Put a single character at (x, y) with optional color
37
+ #
38
+ # @param x [Integer] X position (0-indexed)
39
+ # @param y [Integer] Y position (0-indexed)
40
+ # @param char [String] Character to put
41
+ # @param fg [String, nil] Foreground ANSI color code
42
+ # @param bg [String, nil] Background ANSI color code
43
+ # @param width [Integer, nil] Display width (auto-detected if not provided)
44
+ def put(x, y, char, fg: nil, bg: nil, width: nil)
45
+ return if out_of_bounds?(x, y)
46
+
47
+ # Phase1: Width is calculated once here (not in rendering loop)
48
+ char_width = width || TextUtils.display_width(char)
49
+ @cells[y][x] = {
50
+ char: char,
51
+ fg: fg,
52
+ bg: bg,
53
+ width: char_width
54
+ }
55
+
56
+ # Phase1: Mark row as dirty
57
+ @dirty_rows.add(y)
58
+
59
+ # For full-width characters, mark the next cell as occupied
60
+ if char_width >= 2 && x + 1 < @width
61
+ (char_width - 1).times do |offset|
62
+ next_x = x + 1 + offset
63
+ break if next_x >= @width
64
+ @cells[y][next_x] = {
65
+ char: '',
66
+ fg: nil,
67
+ bg: nil,
68
+ width: 0
69
+ }
70
+ end
71
+ end
72
+ end
73
+
74
+ # Put a string starting at (x, y)
75
+ #
76
+ # @param x [Integer] Starting X position
77
+ # @param y [Integer] Y position
78
+ # @param str [String] String to put (ANSI codes will be stripped)
79
+ # @param fg [String, nil] Foreground ANSI color code
80
+ # @param bg [String, nil] Background ANSI color code
81
+ def put_string(x, y, str, fg: nil, bg: nil)
82
+ return if out_of_bounds?(x, y)
83
+
84
+ # Phase1: ANSI stripping only once (minimal processing)
85
+ # Only strip if the string contains ANSI codes
86
+ clean_str = str.include?("\e") ? ColorHelper.strip_ansi(str) : str
87
+
88
+ current_x = x
89
+ clean_str.each_char do |char|
90
+ break if current_x >= @width
91
+
92
+ char_width = TextUtils.display_width(char)
93
+ put(current_x, y, char, fg: fg, bg: bg, width: char_width)
94
+ current_x += char_width
95
+ end
96
+ end
97
+
98
+ # Get the cell at (x, y)
99
+ #
100
+ # @param x [Integer] X position
101
+ # @param y [Integer] Y position
102
+ # @return [Hash] Cell data {char:, fg:, bg:, width:}
103
+ def get_cell(x, y)
104
+ return default_cell if out_of_bounds?(x, y)
105
+ @cells[y][x]
106
+ end
107
+
108
+ # Get a row as a formatted string
109
+ #
110
+ # @param y [Integer] Row number
111
+ # @return [String] Formatted row with ANSI codes
112
+ def row(y)
113
+ return " " * @width if y < 0 || y >= @height
114
+
115
+ # Phase1: Pre-allocate string capacity for better performance
116
+ result = String.new(capacity: @width * 20)
117
+ current_width = 0 # Phase1: Accumulate width from cells (no recalculation)
118
+
119
+ @cells[y].each do |cell|
120
+ # Skip marker cells for full-width characters
121
+ next if cell[:width] == 0
122
+
123
+ result << format_cell(cell)
124
+ current_width += cell[:width] # Phase1: Use pre-calculated width
125
+ end
126
+
127
+ # Pad the row to full width
128
+ # Phase1: No ANSI stripping or width recalculation needed
129
+ if current_width < @width
130
+ result << (" " * (@width - current_width))
131
+ end
132
+
133
+ result
134
+ end
135
+
136
+ # Clear the entire screen
137
+ def clear
138
+ @cells.each do |row|
139
+ row.fill { default_cell }
140
+ end
141
+ # Phase1: Clear dirty rows after full clear
142
+ @dirty_rows.clear
143
+ end
144
+
145
+ # Phase1: Get dirty rows (rows that have been modified since last clear)
146
+ #
147
+ # @return [Array<Integer>] Array of dirty row indices
148
+ def dirty_rows
149
+ @dirty_rows.to_a
150
+ end
151
+
152
+ # Phase1: Clear dirty row tracking
153
+ def clear_dirty
154
+ @dirty_rows.clear
155
+ end
156
+
157
+ private
158
+
159
+ def default_cell
160
+ { char: ' ', fg: nil, bg: nil, width: 1 }
161
+ end
162
+
163
+ def out_of_bounds?(x, y)
164
+ x < 0 || y < 0 || x >= @width || y >= @height
165
+ end
166
+
167
+ def format_cell(cell)
168
+ char = cell[:char]
169
+ fg = cell[:fg]
170
+ bg = cell[:bg]
171
+
172
+ # Phase1: Fast path for cells without color
173
+ return char if fg.nil? && bg.nil?
174
+
175
+ # Phase1: String builder with pre-allocated capacity (no array generation)
176
+ result = String.new(capacity: 30)
177
+ result << fg if fg
178
+ result << bg if bg
179
+ result << char
180
+ result << "\e[0m"
181
+ result
182
+ end
183
+ end
184
+ end