rufio 0.33.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.
@@ -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