Class: Muxr::Terminal
- Inherits:
-
Object
- Object
- Muxr::Terminal
- Defined in:
- lib/muxr/terminal.rb
Overview
A minimal VT100/ANSI terminal emulator. It maintains a fixed grid of cells plus a cursor and parser state. Bytes fed from a PTY are interpreted into mutations of the grid which the Renderer then composites into the final frame. The emulator implements enough of the protocol to host typical interactive shells (bash, zsh) and line-oriented programs.
Defined Under Namespace
Classes: Cell
Constant Summary collapse
- BOLD =
1- UNDERLINE =
2- REVERSE =
4- DIM =
8- SCROLLBACK_MAX =
5000- OSC_MAX_LEN =
Cap on the OSC payload we buffer before parsing. URLs in OSC 8 can be long but rarely exceed a few hundred bytes; 4 KiB lets the parser stay tolerant of weird inputs without giving an attacker an unbounded sink.
4096- SYNC_TIMEOUT =
Inner programs (fzf ≥ 0.41, neovim, helix, …) bracket coherent screen updates with ‘e[?2026h … e[?2026l` (DECSET 2026 — “Synchronized Output”). When we see the open, we know more bytes are coming that belong to the same logical frame; rendering before the close shows a half-painted state. SYNC_TIMEOUT is the safety cap so a crashed inner program (which left ?2026h open) cannot wedge the pane indefinitely.
0.2
Instance Attribute Summary collapse
-
#cols ⇒ Object
readonly
Returns the value of attribute cols.
-
#cursor_col ⇒ Object
readonly
Returns the value of attribute cursor_col.
-
#cursor_row ⇒ Object
readonly
Returns the value of attribute cursor_row.
-
#rows ⇒ Object
readonly
Returns the value of attribute rows.
-
#search_current ⇒ Object
readonly
———- search ———-.
-
#search_direction ⇒ Object
readonly
———- search ———-.
-
#search_matches ⇒ Object
readonly
———- search ———-.
-
#search_query ⇒ Object
readonly
———- search ———-.
-
#selection_mode ⇒ Object
readonly
Returns the value of attribute selection_mode.
-
#view_offset ⇒ Object
readonly
Returns the value of attribute view_offset.
Instance Method Summary collapse
-
#anchor_selection!(mode: :linear) ⇒ Object
Drop the anchor at the cursor’s current position.
- #cell(r, c) ⇒ Object
- #cell_in_match?(visible_r, c) ⇒ Boolean
-
#clear_anchor! ⇒ Object
Drop the anchor but keep the cursor so the user can continue navigating (vim’s behavior when pressing v while already in linear visual mode).
- #clear_dirty! ⇒ Object
- #clear_search ⇒ Object
- #clear_selection ⇒ Object
- #dirty? ⇒ Boolean
-
#dump_text ⇒ Object
Return the currently-visible grid as a text string (rows joined by “n”, trailing whitespace stripped on each row).
- #extract_selection_text ⇒ Object
- #feed(data) ⇒ Object
-
#find_in_direction(direction) ⇒ Object
Move to the next/previous match in the given direction, wrapping.
-
#initialize(rows: 24, cols: 80) ⇒ Terminal
constructor
A new instance of Terminal.
- #move_selection_cursor_by(dr, dc) ⇒ Object
-
#place_selection_cursor(r, c) ⇒ Object
Place the moving cursor at a viewport position without dropping an anchor — the user is still navigating, not yet selecting.
- #resize(rows, cols) ⇒ Object
- #scroll_back(n = 1) ⇒ Object
- #scroll_forward(n = 1) ⇒ Object
- #scroll_to_bottom ⇒ Object
- #scroll_to_top ⇒ Object
- #scrollback_size ⇒ Object
- #scrolled_back? ⇒ Boolean
- #search(query, direction: :forward) ⇒ Object
- #search_active? ⇒ Boolean
- #selected_at_visible?(r, c) ⇒ Boolean
-
#selection_active? ⇒ Boolean
———- selection ———-.
- #selection_cursor_to(tr, tc) ⇒ Object
- #selection_cursor_to_bottom ⇒ Object
- #selection_cursor_to_first_non_blank ⇒ Object
- #selection_cursor_to_line_end ⇒ Object
- #selection_cursor_to_line_start ⇒ Object
- #selection_cursor_to_top ⇒ Object
-
#selection_cursor_to_viewport(where) ⇒ Object
Jump to top/middle/bottom of the visible viewport (vim H/M/L), landing on the first non-blank column of the destination line.
- #selection_cursor_visible ⇒ Object
- #selection_cursor_word_backward(big: false) ⇒ Object
- #selection_cursor_word_end(big: false) ⇒ Object
- #selection_cursor_word_forward(big: false) ⇒ Object
-
#start_selection_at_visible(r, c, mode: :linear) ⇒ Object
Convenience for tests: place cursor at (r,c) AND anchor immediately.
- #sync_deadline ⇒ Object
-
#sync_pending? ⇒ Boolean
True iff the inner program has opened a synchronized-output block (e[?2026h) and not yet closed it, and the safety timeout has not elapsed. The Application uses this to defer rendering so the diff lands on a fully-formed frame instead of a half-painted one..
-
#take_pending_replies! ⇒ Object
Bytes the emulator owes back to the inner program in response to a query (currently DSR / Device Status Report — ‘e[5n` and `e[6n`). The Pane drains this after each feed and writes it to the PTY’s input side. Without it, programs like the AWS CLI fall back with a warning (“your terminal doesn’t support cursor position requests (CPR)”)..
-
#visible_cell(r, c) ⇒ Object
Returns the Cell that should be visible at (r, c) given the current scrollback view_offset.
Constructor Details
#initialize(rows: 24, cols: 80) ⇒ Terminal
Returns a new instance of Terminal.
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
# File 'lib/muxr/terminal.rb', line 48 def initialize(rows: 24, cols: 80) @rows = rows @cols = cols @buffer = Array.new(rows) { Array.new(cols) { blank_cell } } @cursor_row = 0 @cursor_col = 0 @saved_cursor = [0, 0] @fg = nil @bg = nil @attrs = 0 @autowrap_pending = false @scroll_top = 0 @scroll_bottom = rows - 1 @parser_state = :ground @parser_params = +"" @parser_osc = +"" @feed_remainder = +"".b # Currently-active OSC 8 hyperlink body (the "8;params;URI" payload that # we'll wrap back around runs of cells when rendering), or nil when no # hyperlink is open. Interned via @hyperlink_intern so repeated identical # links share one frozen string for fast equality and small memory. @current_hyperlink = nil @hyperlink_intern = {} @dirty = true @scrollback = [] @view_offset = 0 @selection_anchor = nil @selection_cursor = nil @selection_mode = :linear @sync_pending = false @sync_started_at = nil @pending_replies = +"".b @search_query = nil @search_direction = :forward @search_matches = [] @search_current = nil end |
Instance Attribute Details
#cols ⇒ Object (readonly)
Returns the value of attribute cols.
46 47 48 |
# File 'lib/muxr/terminal.rb', line 46 def cols @cols end |
#cursor_col ⇒ Object (readonly)
Returns the value of attribute cursor_col.
46 47 48 |
# File 'lib/muxr/terminal.rb', line 46 def cursor_col @cursor_col end |
#cursor_row ⇒ Object (readonly)
Returns the value of attribute cursor_row.
46 47 48 |
# File 'lib/muxr/terminal.rb', line 46 def cursor_row @cursor_row end |
#rows ⇒ Object (readonly)
Returns the value of attribute rows.
46 47 48 |
# File 'lib/muxr/terminal.rb', line 46 def rows @rows end |
#search_current ⇒ Object (readonly)
———- search ———-
Substring search over the full timeline (scrollback + live buffer). Smart-case: case-insensitive if the query is all-lowercase, sensitive otherwise. Matches are kept in timeline coordinates so they stay anchored to the same text as the user pages history.
365 366 367 |
# File 'lib/muxr/terminal.rb', line 365 def search_current @search_current end |
#search_direction ⇒ Object (readonly)
———- search ———-
Substring search over the full timeline (scrollback + live buffer). Smart-case: case-insensitive if the query is all-lowercase, sensitive otherwise. Matches are kept in timeline coordinates so they stay anchored to the same text as the user pages history.
365 366 367 |
# File 'lib/muxr/terminal.rb', line 365 def search_direction @search_direction end |
#search_matches ⇒ Object (readonly)
———- search ———-
Substring search over the full timeline (scrollback + live buffer). Smart-case: case-insensitive if the query is all-lowercase, sensitive otherwise. Matches are kept in timeline coordinates so they stay anchored to the same text as the user pages history.
365 366 367 |
# File 'lib/muxr/terminal.rb', line 365 def search_matches @search_matches end |
#search_query ⇒ Object (readonly)
———- search ———-
Substring search over the full timeline (scrollback + live buffer). Smart-case: case-insensitive if the query is all-lowercase, sensitive otherwise. Matches are kept in timeline coordinates so they stay anchored to the same text as the user pages history.
365 366 367 |
# File 'lib/muxr/terminal.rb', line 365 def search_query @search_query end |
#selection_mode ⇒ Object (readonly)
Returns the value of attribute selection_mode.
117 118 119 |
# File 'lib/muxr/terminal.rb', line 117 def selection_mode @selection_mode end |
#view_offset ⇒ Object (readonly)
Returns the value of attribute view_offset.
46 47 48 |
# File 'lib/muxr/terminal.rb', line 46 def view_offset @view_offset end |
Instance Method Details
#anchor_selection!(mode: :linear) ⇒ Object
Drop the anchor at the cursor’s current position. ‘mode` controls the selection shape: :linear (character-by-character, reading order) or :block (rectangular).
202 203 204 205 206 207 |
# File 'lib/muxr/terminal.rb', line 202 def anchor_selection!(mode: :linear) return unless @selection_cursor @selection_anchor = @selection_cursor.dup @selection_mode = mode @dirty = true end |
#cell(r, c) ⇒ Object
119 120 121 |
# File 'lib/muxr/terminal.rb', line 119 def cell(r, c) @buffer[r][c] end |
#cell_in_match?(visible_r, c) ⇒ Boolean
412 413 414 415 416 417 418 419 |
# File 'lib/muxr/terminal.rb', line 412 def cell_in_match?(visible_r, c) return false if @search_matches.empty? tr = timeline_row_for_visible(visible_r) # Linear scan is fine: SCROLLBACK_MAX caps matches at O(rows*cols), and # the renderer touches each visible cell once per frame. A row-indexed # cache would matter at much larger buffer sizes than ours. @search_matches.any? { |mr, sc, ec| mr == tr && c >= sc && c <= ec } end |
#clear_anchor! ⇒ Object
Drop the anchor but keep the cursor so the user can continue navigating (vim’s behavior when pressing v while already in linear visual mode).
211 212 213 214 215 |
# File 'lib/muxr/terminal.rb', line 211 def clear_anchor! return unless @selection_anchor @selection_anchor = nil @dirty = true end |
#clear_dirty! ⇒ Object
488 489 490 |
# File 'lib/muxr/terminal.rb', line 488 def clear_dirty! @dirty = false end |
#clear_search ⇒ Object
425 426 427 428 429 430 431 |
# File 'lib/muxr/terminal.rb', line 425 def clear_search return if @search_query.nil? && @search_matches.empty? @search_query = nil @search_matches = [] @search_current = nil @dirty = true end |
#clear_selection ⇒ Object
351 352 353 354 355 356 |
# File 'lib/muxr/terminal.rb', line 351 def clear_selection return unless @selection_anchor @selection_anchor = nil @selection_cursor = nil @dirty = true end |
#dirty? ⇒ Boolean
484 485 486 |
# File 'lib/muxr/terminal.rb', line 484 def dirty? @dirty end |
#dump_text ⇒ Object
Return the currently-visible grid as a text string (rows joined by “n”, trailing whitespace stripped on each row). Used by the control surface to expose pane contents to programmatic clients (the MCP bridge in particular). This walks visible_cell so callers see whatever the user is currently looking at, including scrollback.
128 129 130 131 132 133 134 135 |
# File 'lib/muxr/terminal.rb', line 128 def dump_text lines = Array.new(@rows) do |r| row = String.new(capacity: @cols) @cols.times { |c| row << visible_cell(r, c).char } row.rstrip end lines.join("\n") end |
#extract_selection_text ⇒ Object
447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 |
# File 'lib/muxr/terminal.rb', line 447 def extract_selection_text return "" unless @selection_anchor if @selection_mode == :block ar, ac = @selection_anchor br, bc = @selection_cursor min_r, max_r = ar <= br ? [ar, br] : [br, ar] min_c, max_c = ac <= bc ? [ac, bc] : [bc, ac] lines = [] (min_r..max_r).each do |tr| row = timeline_row(tr) if row.nil? || min_c >= row.length lines << "" next end last = [max_c, row.length - 1].min chars = (min_c..last).map { |c| row[c]&.char || " " } lines << chars.join.rstrip end return lines.join("\n") end sr, sc, er, ec = ordered_selection lines = [] (sr..er).each do |tr| row = timeline_row(tr) if row.nil? lines << "" next end first = (tr == sr) ? sc : 0 last = (tr == er) ? ec : row.length - 1 last = [last, row.length - 1].min chars = (first..last).map { |c| row[c]&.char || " " } lines << chars.join.rstrip end lines.join("\n") end |
#feed(data) ⇒ Object
518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 |
# File 'lib/muxr/terminal.rb', line 518 def feed(data) bytes = @feed_remainder + data.b @feed_remainder = +"".b str = bytes.dup.force_encoding(Encoding::UTF_8) unless str.valid_encoding? # Find the longest valid UTF-8 prefix and stash the remainder for the # next feed call so multi-byte characters don't get garbled across PTY # read boundaries. raw = bytes.bytes while raw.any? candidate = raw.pack("C*").force_encoding(Encoding::UTF_8) break if candidate.valid_encoding? @feed_remainder = ([raw.last] + @feed_remainder.bytes).pack("C*").b raw.pop end str = raw.pack("C*").force_encoding(Encoding::UTF_8) # Bail out completely if we couldn't decode anything yet. return if str.empty? end str.each_char { |c| process_char(c) } @dirty = true end |
#find_in_direction(direction) ⇒ Object
Move to the next/previous match in the given direction, wrapping. Returns the new current match index, or nil if there are no matches. Anchors on the current match (not the viewport top) so n always advances even when scroll_view_to_match has centered the previous hit and dragged the viewport top behind it.
393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 |
# File 'lib/muxr/terminal.rb', line 393 def find_in_direction(direction) return nil if @search_matches.empty? anchor_tr = if @search_current && @search_matches[@search_current] @search_matches[@search_current][0] else current_search_anchor_row end idx = strict_next_in_direction(anchor_tr, direction) if idx.nil? # Wrap: pick the far end depending on direction. idx = direction == :forward ? 0 : @search_matches.length - 1 end @search_current = idx scroll_view_to_match(idx) @dirty = true idx end |
#move_selection_cursor_by(dr, dc) ⇒ Object
223 224 225 226 227 228 229 230 231 232 |
# File 'lib/muxr/terminal.rb', line 223 def move_selection_cursor_by(dr, dc) return unless @selection_cursor tr, tc = @selection_cursor ntr = (tr + dr).clamp(0, timeline_size - 1) ntc = (tc + dc).clamp(0, @cols - 1) return if ntr == tr && ntc == tc @selection_cursor = [ntr, ntc] ensure_selection_cursor_visible @dirty = true end |
#place_selection_cursor(r, c) ⇒ Object
Place the moving cursor at a viewport position without dropping an anchor — the user is still navigating, not yet selecting.
191 192 193 194 195 196 197 |
# File 'lib/muxr/terminal.rb', line 191 def place_selection_cursor(r, c) tr = timeline_row_for_visible(r).clamp(0, timeline_size - 1) tc = c.clamp(0, @cols - 1) @selection_cursor = [tr, tc] @selection_anchor = nil @dirty = true end |
#resize(rows, cols) ⇒ Object
492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 |
# File 'lib/muxr/terminal.rb', line 492 def resize(rows, cols) return if rows == @rows && cols == @cols new_buf = Array.new(rows) { Array.new(cols) { blank_cell } } keep_rows = [rows, @rows].min keep_cols = [cols, @cols].min src_start = @rows - keep_rows keep_rows.times do |i| keep_cols.times do |j| new_buf[i][j].copy_from(@buffer[src_start + i][j]) end end @buffer = new_buf @rows = rows @cols = cols @scroll_top = 0 @scroll_bottom = rows - 1 @cursor_row = @cursor_row.clamp(0, rows - 1) @cursor_col = @cursor_col.clamp(0, cols - 1) @autowrap_pending = false # Selection points at timeline rows whose shape can't be remapped # meaningfully through a resize, so drop it rather than show a smear. @selection_anchor = nil @selection_cursor = nil @dirty = true end |
#scroll_back(n = 1) ⇒ Object
161 162 163 |
# File 'lib/muxr/terminal.rb', line 161 def scroll_back(n = 1) set_view_offset(@view_offset + n) end |
#scroll_forward(n = 1) ⇒ Object
165 166 167 |
# File 'lib/muxr/terminal.rb', line 165 def scroll_forward(n = 1) set_view_offset(@view_offset - n) end |
#scroll_to_bottom ⇒ Object
173 174 175 |
# File 'lib/muxr/terminal.rb', line 173 def scroll_to_bottom set_view_offset(0) end |
#scroll_to_top ⇒ Object
169 170 171 |
# File 'lib/muxr/terminal.rb', line 169 def scroll_to_top set_view_offset(@scrollback.size) end |
#scrollback_size ⇒ Object
153 154 155 |
# File 'lib/muxr/terminal.rb', line 153 def scrollback_size @scrollback.size end |
#scrolled_back? ⇒ Boolean
157 158 159 |
# File 'lib/muxr/terminal.rb', line 157 def scrolled_back? @view_offset > 0 end |
#search(query, direction: :forward) ⇒ Object
367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 |
# File 'lib/muxr/terminal.rb', line 367 def search(query, direction: :forward) query = query.to_s if query.empty? clear_search return 0 end @search_query = query @search_direction = direction @search_matches = collect_matches(query) if @search_matches.empty? @search_current = nil @dirty = true return 0 end @search_current = nearest_match_in_direction(current_search_anchor_row, direction, inclusive: true) @search_current ||= direction == :forward ? 0 : @search_matches.length - 1 scroll_view_to_match(@search_current) @dirty = true @search_matches.length end |
#search_active? ⇒ Boolean
421 422 423 |
# File 'lib/muxr/terminal.rb', line 421 def search_active? !(@search_query.nil? || @search_matches.empty?) end |
#selected_at_visible?(r, c) ⇒ Boolean
433 434 435 436 437 |
# File 'lib/muxr/terminal.rb', line 433 def selected_at_visible?(r, c) return false unless @selection_anchor tr = timeline_row_for_visible(r) inside_selection?(tr, c) end |
#selection_active? ⇒ Boolean
———- selection ———-
Selection coordinates are in the combined “timeline”:
0..scrollback.size-1 → @scrollback rows
scrollback.size..scrollback.size+rows-1 → @buffer rows
so the selection stays anchored to the same text as the user pages through history.
185 186 187 |
# File 'lib/muxr/terminal.rb', line 185 def selection_active? !@selection_anchor.nil? end |
#selection_cursor_to(tr, tc) ⇒ Object
234 235 236 237 238 239 240 241 |
# File 'lib/muxr/terminal.rb', line 234 def selection_cursor_to(tr, tc) return unless @selection_cursor ntr = tr.clamp(0, timeline_size - 1) ntc = tc.clamp(0, @cols - 1) @selection_cursor = [ntr, ntc] ensure_selection_cursor_visible @dirty = true end |
#selection_cursor_to_bottom ⇒ Object
257 258 259 |
# File 'lib/muxr/terminal.rb', line 257 def selection_cursor_to_bottom selection_cursor_to(timeline_size - 1, @cols - 1) end |
#selection_cursor_to_first_non_blank ⇒ Object
261 262 263 264 265 |
# File 'lib/muxr/terminal.rb', line 261 def selection_cursor_to_first_non_blank return unless @selection_cursor tr = @selection_cursor[0] selection_cursor_to(tr, first_non_blank_col(tr)) end |
#selection_cursor_to_line_end ⇒ Object
248 249 250 251 |
# File 'lib/muxr/terminal.rb', line 248 def selection_cursor_to_line_end return unless @selection_cursor selection_cursor_to(@selection_cursor[0], @cols - 1) end |
#selection_cursor_to_line_start ⇒ Object
243 244 245 246 |
# File 'lib/muxr/terminal.rb', line 243 def selection_cursor_to_line_start return unless @selection_cursor selection_cursor_to(@selection_cursor[0], 0) end |
#selection_cursor_to_top ⇒ Object
253 254 255 |
# File 'lib/muxr/terminal.rb', line 253 def selection_cursor_to_top selection_cursor_to(0, 0) end |
#selection_cursor_to_viewport(where) ⇒ Object
Jump to top/middle/bottom of the visible viewport (vim H/M/L), landing on the first non-blank column of the destination line.
269 270 271 272 273 274 275 276 277 278 279 |
# File 'lib/muxr/terminal.rb', line 269 def (where) return unless @selection_cursor vr = case where when :top then 0 when :middle then @rows / 2 when :bottom then @rows - 1 end return if vr.nil? tr = timeline_row_for_visible(vr).clamp(0, timeline_size - 1) selection_cursor_to(tr, first_non_blank_col(tr)) end |
#selection_cursor_visible ⇒ Object
439 440 441 442 443 444 445 |
# File 'lib/muxr/terminal.rb', line 439 def selection_cursor_visible return nil unless @selection_cursor tr, tc = @selection_cursor vr = tr - (@scrollback.size - @view_offset) return nil unless vr.between?(0, @rows - 1) [vr, tc] end |
#selection_cursor_word_backward(big: false) ⇒ Object
326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 |
# File 'lib/muxr/terminal.rb', line 326 def selection_cursor_word_backward(big: false) return unless @selection_cursor tr, tc = @selection_cursor pos = step_backward(tr, tc) return unless pos tr, tc = pos while char_class_at(tr, tc, big: big) == :space pos = step_backward(tr, tc) unless pos selection_cursor_to(tr, tc) return end tr, tc = pos end cls = char_class_at(tr, tc, big: big) loop do pos = step_backward(tr, tc) if pos.nil? || pos[0] != tr || char_class_at(pos[0], pos[1], big: big) != cls selection_cursor_to(tr, tc) return end tr, tc = pos end end |
#selection_cursor_word_end(big: false) ⇒ Object
303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 |
# File 'lib/muxr/terminal.rb', line 303 def selection_cursor_word_end(big: false) return unless @selection_cursor tr, tc = @selection_cursor pos = step_forward(tr, tc) return unless pos tr, tc = pos while char_class_at(tr, tc, big: big) == :space pos = step_forward(tr, tc) break unless pos tr, tc = pos end return if char_class_at(tr, tc, big: big) == :space cls = char_class_at(tr, tc, big: big) loop do pos = step_forward(tr, tc) if pos.nil? || pos[0] != tr || char_class_at(pos[0], pos[1], big: big) != cls selection_cursor_to(tr, tc) return end tr, tc = pos end end |
#selection_cursor_word_forward(big: false) ⇒ Object
281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 |
# File 'lib/muxr/terminal.rb', line 281 def selection_cursor_word_forward(big: false) return unless @selection_cursor tr, tc = @selection_cursor prev_cls = char_class_at(tr, tc, big: big) loop do nxt = step_forward(tr, tc) break unless nxt ntr, ntc = nxt cur_cls = char_class_at(ntr, ntc, big: big) # Row boundaries act as whitespace breaks even when the row is fully # packed (no trailing pad) — visually the user sees a new line. effective_prev = (ntr != tr) ? :space : prev_cls if effective_prev != cur_cls && cur_cls != :space selection_cursor_to(ntr, ntc) return end tr, tc = ntr, ntc prev_cls = cur_cls end selection_cursor_to(timeline_size - 1, @cols - 1) end |
#start_selection_at_visible(r, c, mode: :linear) ⇒ Object
Convenience for tests: place cursor at (r,c) AND anchor immediately.
218 219 220 221 |
# File 'lib/muxr/terminal.rb', line 218 def start_selection_at_visible(r, c, mode: :linear) place_selection_cursor(r, c) anchor_selection!(mode: mode) end |
#sync_deadline ⇒ Object
112 113 114 115 |
# File 'lib/muxr/terminal.rb', line 112 def sync_deadline return nil unless @sync_pending && @sync_started_at @sync_started_at + SYNC_TIMEOUT end |
#sync_pending? ⇒ Boolean
True iff the inner program has opened a synchronized-output block (e[?2026h) and not yet closed it, and the safety timeout has not elapsed. The Application uses this to defer rendering so the diff lands on a fully-formed frame instead of a half-painted one.
102 103 104 105 106 107 108 109 110 |
# File 'lib/muxr/terminal.rb', line 102 def sync_pending? return false unless @sync_pending if @sync_started_at && (Process.clock_gettime(Process::CLOCK_MONOTONIC) - @sync_started_at) > SYNC_TIMEOUT @sync_pending = false @sync_started_at = nil return false end true end |
#take_pending_replies! ⇒ Object
Bytes the emulator owes back to the inner program in response to a query (currently DSR / Device Status Report — ‘e[5n` and `e[6n`). The Pane drains this after each feed and writes it to the PTY’s input side. Without it, programs like the AWS CLI fall back with a warning (“your terminal doesn’t support cursor position requests (CPR)”).
91 92 93 94 95 96 |
# File 'lib/muxr/terminal.rb', line 91 def take_pending_replies! return nil if @pending_replies.empty? data = @pending_replies @pending_replies = +"".b data end |
#visible_cell(r, c) ⇒ Object
Returns the Cell that should be visible at (r, c) given the current scrollback view_offset. When view_offset == 0 this is the live grid. When view_offset > 0, rows in the top of the visible area are sourced from @scrollback instead.
141 142 143 144 145 146 147 148 149 150 151 |
# File 'lib/muxr/terminal.rb', line 141 def visible_cell(r, c) return @buffer[r][c] if @view_offset.zero? idx = @scrollback.size - @view_offset + r if idx < @scrollback.size row = @scrollback[idx] return blank_cell if row.nil? || c >= row.length row[c] else @buffer[idx - @scrollback.size][c] end end |