Class: Clacky::UI2::ScreenBuffer
- Inherits:
-
Object
- Object
- Clacky::UI2::ScreenBuffer
- Defined in:
- lib/clacky/ui2/screen_buffer.rb
Overview
ScreenBuffer manages terminal screen state and provides low-level rendering primitives
Instance Attribute Summary collapse
-
#height ⇒ Object
readonly
Returns the value of attribute height.
-
#width ⇒ Object
readonly
Returns the value of attribute width.
Instance Method Summary collapse
-
#clear_line ⇒ Object
Clear current line.
-
#clear_screen(mode: :preserve) ⇒ Object
Clear screen with different modes: :preserve - clear visible screen, scrollback history preserved (default) :current - cursor to top-left and erase to end, no new scrollback produced :reset - clear visible screen AND scrollback history (full reset).
-
#clear_to_eol ⇒ Object
Clear from cursor to end of line.
-
#disable_alt_screen ⇒ Object
Disable alternative screen buffer.
-
#disable_raw_mode ⇒ Object
Disable raw mode.
-
#enable_alt_screen ⇒ Object
Enable alternative screen buffer (like vim/less).
-
#enable_raw_mode ⇒ Object
Enable raw mode (disable line buffering).
-
#flush ⇒ Object
Flush output.
-
#hide_cursor ⇒ Object
Hide cursor.
-
#initialize ⇒ ScreenBuffer
constructor
A new instance of ScreenBuffer.
-
#move_cursor(row, col) ⇒ Object
Move cursor to specific position (0-indexed).
-
#read_char(timeout: nil) ⇒ String?
Read a single character without echo.
-
#read_key(timeout: nil) ⇒ Symbol, ...
Read a key including special keys (arrows, etc.).
-
#reset_scroll_region ⇒ Object
Reset scroll region to full screen.
-
#restore_cursor ⇒ Object
Restore cursor position.
-
#save_cursor ⇒ Object
Save cursor position.
-
#scroll_down(n = 1) ⇒ Object
Scroll the scroll region down by n lines.
-
#scroll_up(n = 1) ⇒ Object
Scroll the scroll region up by n lines.
-
#set_scroll_region(top, bottom) ⇒ Object
Set scroll region (DECSTBM - DEC Set Top and Bottom Margins) Content written in this region will scroll, content outside will stay fixed.
-
#show_cursor ⇒ Object
Show cursor.
-
#update_dimensions ⇒ Object
Get current screen dimensions.
Constructor Details
#initialize ⇒ ScreenBuffer
Returns a new instance of ScreenBuffer.
13 14 15 16 17 18 19 20 21 22 23 |
# File 'lib/clacky/ui2/screen_buffer.rb', line 13 def initialize @width = TTY::Screen.width @height = TTY::Screen.height @buffer = [] @last_input_time = nil @rapid_input_threshold = 0.01 # 10ms threshold for detecting paste-like rapid input # Keep stdin in UTF-8 mode so getc returns complete multi-byte characters (e.g. CJK). # Switching to BINARY would cause getc to return one byte at a time, breaking Chinese input. $stdin.set_encoding('UTF-8') end |
Instance Attribute Details
#height ⇒ Object (readonly)
Returns the value of attribute height.
11 12 13 |
# File 'lib/clacky/ui2/screen_buffer.rb', line 11 def height @height end |
#width ⇒ Object (readonly)
Returns the value of attribute width.
11 12 13 |
# File 'lib/clacky/ui2/screen_buffer.rb', line 11 def width @width end |
Instance Method Details
#clear_line ⇒ Object
Clear current line
51 52 53 |
# File 'lib/clacky/ui2/screen_buffer.rb', line 51 def clear_line print "\e[2K" end |
#clear_screen(mode: :preserve) ⇒ Object
Clear screen with different modes:
:preserve - clear visible screen, scrollback history preserved (default)
:current - cursor to top-left and erase to end, no new scrollback produced
:reset - clear visible screen AND scrollback history (full reset)
37 38 39 40 41 42 43 44 45 46 47 48 |
# File 'lib/clacky/ui2/screen_buffer.rb', line 37 def clear_screen(mode: :preserve) case mode when :reset print "\e[3J" # erase scrollback buffer print "\e[H\e[J" # cursor to top-left, erase to end of screen when :current print "\e[H\e[J" # cursor to top-left, erase to end of screen else # :preserve print "\e[2J\e[H" # erase visible screen, scrollback preserved end move_cursor(0, 0) end |
#clear_to_eol ⇒ Object
Clear from cursor to end of line
56 57 58 |
# File 'lib/clacky/ui2/screen_buffer.rb', line 56 def clear_to_eol print "\e[K" end |
#disable_alt_screen ⇒ Object
Disable alternative screen buffer
86 87 88 |
# File 'lib/clacky/ui2/screen_buffer.rb', line 86 def disable_alt_screen print "\e[?1049l" end |
#disable_raw_mode ⇒ Object
Disable raw mode
127 128 129 |
# File 'lib/clacky/ui2/screen_buffer.rb', line 127 def disable_raw_mode $stdin.cooked! end |
#enable_alt_screen ⇒ Object
Enable alternative screen buffer (like vim/less)
81 82 83 |
# File 'lib/clacky/ui2/screen_buffer.rb', line 81 def enable_alt_screen print "\e[?1049h" end |
#enable_raw_mode ⇒ Object
Enable raw mode (disable line buffering)
122 123 124 |
# File 'lib/clacky/ui2/screen_buffer.rb', line 122 def enable_raw_mode $stdin.raw! end |
#flush ⇒ Object
Flush output
254 255 256 |
# File 'lib/clacky/ui2/screen_buffer.rb', line 254 def flush $stdout.flush end |
#hide_cursor ⇒ Object
Hide cursor
61 62 63 |
# File 'lib/clacky/ui2/screen_buffer.rb', line 61 def hide_cursor print "\e[?25l" end |
#move_cursor(row, col) ⇒ Object
Move cursor to specific position (0-indexed)
28 29 30 |
# File 'lib/clacky/ui2/screen_buffer.rb', line 28 def move_cursor(row, col) print "\e[#{row + 1};#{col + 1}H" end |
#read_char(timeout: nil) ⇒ String?
Read a single character without echo
134 135 136 137 138 139 140 |
# File 'lib/clacky/ui2/screen_buffer.rb', line 134 def read_char(timeout: nil) if timeout return nil unless IO.select([$stdin], nil, nil, timeout) end $stdin.getc end |
#read_key(timeout: nil) ⇒ Symbol, ...
Read a key including special keys (arrows, etc.)
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 |
# File 'lib/clacky/ui2/screen_buffer.rb', line 145 def read_key(timeout: nil) current_time = Time.now.to_f is_rapid_input = @last_input_time && (current_time - @last_input_time) < @rapid_input_threshold @last_input_time = current_time char = read_char(timeout: timeout) return nil unless char # Convert raw BINARY bytes to valid UTF-8. Invalid/undefined bytes are dropped # rather than raising ArgumentError (which would crash the input loop). char = safe_to_utf8(char) if char.is_a?(String) # Handle escape sequences for special keys if char == "\e" # Non-blocking read for escape sequence char2 = read_char(timeout: 0.01) return :escape unless char2 if char2 == "[" char3 = read_char(timeout: 0.01) case char3 when "A" then return :up_arrow when "B" then return :down_arrow when "C" then return :right_arrow when "D" then return :left_arrow when "H" then return :home when "F" then return :end when "Z" then return :shift_tab when "3" char4 = read_char(timeout: 0.01) return :delete if char4 == "~" end end end # Check if there are more characters available (for rapid input detection) has_more_input = IO.select([$stdin], nil, nil, 0) # If this is rapid input or there are more characters available if is_rapid_input || has_more_input buffer = char.to_s.dup # Keep reading available characters loop_count = 0 empty_checks = 0 loop do # Check if there's data available immediately has_data = IO.select([$stdin], nil, nil, 0) if has_data next_char = $stdin.getc rescue nil break unless next_char next_char = safe_to_utf8(next_char) buffer << next_char loop_count += 1 empty_checks = 0 # Reset empty check counter else # No immediate data, but wait a bit to see if more is coming # This handles the case where paste data arrives in chunks empty_checks += 1 if empty_checks == 1 # First empty check - wait 10ms for more data sleep 0.01 else # Second empty check - really no more data break end end end # If we buffered multiple characters or newlines, treat as rapid input (paste) if buffer.length > 1 || buffer.include?("\n") || buffer.include?("\r") # Ensure the accumulated buffer is valid UTF-8 before regex operations buffer = safe_to_utf8(buffer) # Remove any trailing \r or \n from rapid input buffer cleaned_buffer = buffer.gsub(/[\r\n]+\z/, '') return { type: :rapid_input, text: cleaned_buffer } if cleaned_buffer.length > 0 end # Single character, continue to normal handling char = buffer[0] if buffer.length == 1 end # Handle control characters case char when "\r" then :enter when "\n" then :newline # Shift+Enter sends \n when "\u007F", "\b" then :backspace when "\u0001" then :ctrl_a when "\u0002" then :ctrl_b when "\u0003" then :ctrl_c when "\u0004" then :ctrl_d when "\u0005" then :ctrl_e when "\u0006" then :ctrl_f when "\u000B" then :ctrl_k when "\u000C" then :ctrl_l when "\u000F" then :ctrl_o when "\u0012" then :ctrl_r when "\u0015" then :ctrl_u when "\u0016" then :ctrl_v when "\u0017" then :ctrl_w when "\t" then :tab else char end end |
#reset_scroll_region ⇒ Object
Reset scroll region to full screen
99 100 101 |
# File 'lib/clacky/ui2/screen_buffer.rb', line 99 def reset_scroll_region print "\e[r" end |
#restore_cursor ⇒ Object
Restore cursor position
76 77 78 |
# File 'lib/clacky/ui2/screen_buffer.rb', line 76 def restore_cursor print "\e[u" end |
#save_cursor ⇒ Object
Save cursor position
71 72 73 |
# File 'lib/clacky/ui2/screen_buffer.rb', line 71 def save_cursor print "\e[s" end |
#scroll_down(n = 1) ⇒ Object
Scroll the scroll region down by n lines
111 112 113 |
# File 'lib/clacky/ui2/screen_buffer.rb', line 111 def scroll_down(n = 1) print "\e[#{n}T" end |
#scroll_up(n = 1) ⇒ Object
Scroll the scroll region up by n lines
105 106 107 |
# File 'lib/clacky/ui2/screen_buffer.rb', line 105 def scroll_up(n = 1) print "\e[#{n}S" end |
#set_scroll_region(top, bottom) ⇒ Object
Set scroll region (DECSTBM - DEC Set Top and Bottom Margins) Content written in this region will scroll, content outside will stay fixed
94 95 96 |
# File 'lib/clacky/ui2/screen_buffer.rb', line 94 def set_scroll_region(top, bottom) print "\e[#{top};#{bottom}r" end |
#show_cursor ⇒ Object
Show cursor
66 67 68 |
# File 'lib/clacky/ui2/screen_buffer.rb', line 66 def show_cursor print "\e[?25h" end |
#update_dimensions ⇒ Object
Get current screen dimensions
116 117 118 119 |
# File 'lib/clacky/ui2/screen_buffer.rb', line 116 def update_dimensions @width = TTY::Screen.width @height = TTY::Screen.height end |