Class: Echoes::Screen
- Inherits:
-
Object
- Object
- Echoes::Screen
- Defined in:
- lib/echoes/screen.rb
Constant Summary collapse
- DEC_SPECIAL =
{ '`' => "\u{25C6}", 'a' => "\u{2592}", 'b' => "\u{2409}", 'c' => "\u{240C}", 'd' => "\u{240D}", 'e' => "\u{240A}", 'f' => "\u{00B0}", 'g' => "\u{00B1}", 'h' => "\u{2424}", 'i' => "\u{240B}", 'j' => "\u{2518}", 'k' => "\u{2510}", 'l' => "\u{250C}", 'm' => "\u{2514}", 'n' => "\u{253C}", 'o' => "\u{23BA}", 'p' => "\u{23BB}", 'q' => "\u{2500}", 'r' => "\u{23BC}", 's' => "\u{23BD}", 't' => "\u{251C}", 'u' => "\u{2524}", 'v' => "\u{2534}", 'w' => "\u{252C}", 'x' => "\u{2502}", 'y' => "\u{2264}", 'z' => "\u{2265}", '{' => "\u{03C0}", '|' => "\u{2260}", '}' => "\u{00A3}", '~' => "\u{00B7}", }.freeze
Instance Attribute Summary collapse
-
#active_charset ⇒ Object
Returns the value of attribute active_charset.
-
#application_keypad ⇒ Object
Returns the value of attribute application_keypad.
-
#background ⇒ Object
Returns the value of attribute background.
-
#bell ⇒ Object
Returns the value of attribute bell.
-
#bg_fills ⇒ Object
Returns the value of attribute bg_fills.
-
#capture_handler ⇒ Object
Returns the value of attribute capture_handler.
-
#cell_pixel_height ⇒ Object
Returns the value of attribute cell_pixel_height.
-
#cell_pixel_width ⇒ Object
Returns the value of attribute cell_pixel_width.
-
#clipboard_handler ⇒ Object
Returns the value of attribute clipboard_handler.
-
#cols ⇒ Object
readonly
Returns the value of attribute cols.
-
#command_marks ⇒ Object
readonly
Returns the value of attribute command_marks.
-
#current_directory ⇒ Object
Returns the value of attribute current_directory.
-
#cursor ⇒ Object
readonly
Returns the value of attribute cursor.
-
#cursor_style ⇒ Object
Returns the value of attribute cursor_style.
-
#dirty_rows ⇒ Object
readonly
Returns the value of attribute dirty_rows.
-
#display_info_handler ⇒ Object
Returns the value of attribute display_info_handler.
-
#glyph_measurer ⇒ Object
Returns the value of attribute glyph_measurer.
-
#grid ⇒ Object
readonly
Returns the value of attribute grid.
-
#insert_mode ⇒ Object
Returns the value of attribute insert_mode.
-
#mouse_encoding ⇒ Object
Returns the value of attribute mouse_encoding.
-
#mouse_tracking ⇒ Object
Returns the value of attribute mouse_tracking.
-
#notification_handler ⇒ Object
Returns the value of attribute notification_handler.
-
#open_window_handler ⇒ Object
Returns the value of attribute open_window_handler.
-
#palette_handler ⇒ Object
Returns the value of attribute palette_handler.
-
#pending_wrap ⇒ Object
Returns the value of attribute pending_wrap.
-
#placements ⇒ Object
readonly
Tracks what kitty-graphics images are currently visible on this pane’s grid.
-
#rows ⇒ Object
readonly
Returns the value of attribute rows.
-
#scrollback ⇒ Object
readonly
Returns the value of attribute scrollback.
-
#single_shift ⇒ Object
Returns the value of attribute single_shift.
-
#sync_active ⇒ Object
Returns the value of attribute sync_active.
-
#title ⇒ Object
Returns the value of attribute title.
Class Method Summary collapse
Instance Method Summary collapse
-
#adjust_command_marks(delta) ⇒ Object
When scrollback shifts (oldest row dropped), every row index in now point before the scrollback floor are dropped — their content is no longer reachable.
- #application_cursor_keys=(val) ⇒ Object
- #application_cursor_keys? ⇒ Boolean
- #apply_sgr_subparams(sub) ⇒ Object
- #auto_wrap=(val) ⇒ Object
- #auto_wrap? ⇒ Boolean
- #backspace ⇒ Object
- #backward_tab(n = 1) ⇒ Object
- #bracketed_paste_mode=(val) ⇒ Object
- #bracketed_paste_mode? ⇒ Boolean
- #carriage_return ⇒ Object
- #clear_dirty ⇒ Object
- #clear_tab_stop(mode = 0) ⇒ Object
- #clipboard_content ⇒ Object
- #decaln ⇒ Object
- #delete_chars(n = 1) ⇒ Object
- #delete_lines(n = 1) ⇒ Object
- #designate_charset(g, charset) ⇒ Object
- #erase_chars(n = 1) ⇒ Object
- #erase_in_display(mode = 0) ⇒ Object
- #erase_in_line(mode = 0) ⇒ Object
-
#find_command_mark_at_row(abs_row) ⇒ Object
Find the command mark whose prompt+input region covers ‘abs_row`, or nil if no mark covers that row.
- #focus_reporting=(val) ⇒ Object
- #focus_reporting? ⇒ Boolean
- #hide_cursor ⇒ Object
-
#initialize(rows: 24, cols: 80) ⇒ Screen
constructor
A new instance of Screen.
- #insert_chars(n = 1) ⇒ Object
- #insert_lines(n = 1) ⇒ Object
-
#last_completed_command_mark ⇒ Object
Most recently completed command mark (D was emitted).
- #line_feed ⇒ Object
- #mark_all_dirty ⇒ Object
- #mark_dirty(row) ⇒ Object
- #move_cursor(row, col) ⇒ Object
- #move_cursor_backward(n = 1) ⇒ Object
- #move_cursor_down(n = 1) ⇒ Object
- #move_cursor_forward(n = 1) ⇒ Object
- #move_cursor_next_line(n = 1) ⇒ Object
- #move_cursor_prev_line(n = 1) ⇒ Object
- #move_cursor_up(n = 1) ⇒ Object
- #origin_mode=(val) ⇒ Object
- #origin_mode? ⇒ Boolean
-
#osc133_mark(kind, exit_code: nil) ⇒ Object
OSC 133 prompt boundary marker.
-
#output_region_for_row(abs_row) ⇒ Object
Find the command mark whose output region covers ‘abs_row` and return [start_row, end_row] (both inclusive) so callers like the GUI’s triple-click can highlight or copy that whole region.
- #pop_title ⇒ Object
- #push_title ⇒ Object
- #put_char(c) ⇒ Object
-
#put_kitty_image(rgba:, width:, height:, cells_w: nil, cells_h: nil, px_x_offset: 0, px_y_offset: 0, suppress_cursor: false, image_id: nil) ⇒ Object
Place a Kitty-graphics-protocol image as a multicell anchor.
- #put_multicell(text, scale:, width:, frac_n:, frac_d:, valign:, halign:, family: nil, flip_h: false, flip_v: false) ⇒ Object
- #put_sixel(data, params) ⇒ Object
-
#put_styled_segments(segments) ⇒ Object
Write a sequence of styled prompt segments directly into the cell grid, bypassing the ANSI SGR parser.
- #repeat_char(n = 1) ⇒ Object
- #reset ⇒ Object
- #resize(new_rows, new_cols) ⇒ Object
- #restore_cursor ⇒ Object
- #reverse_index ⇒ Object
- #save_cursor ⇒ Object
- #scroll_down(n = 1) ⇒ Object
- #scroll_up(n = 1) ⇒ Object
- #selected_text(sr, sc, er, ec) ⇒ Object
- #set_clipboard(text) ⇒ Object
-
#set_current_command_text(text) ⇒ Object
Attach the literal command text to the most recently opened mark.
- #set_graphics(params) ⇒ Object
- #set_hyperlink(uri) ⇒ Object
- #set_scroll_region(top, bottom) ⇒ Object
- #set_tab_stop ⇒ Object
-
#shift_placements(delta) ⇒ Object
Move every placement anchor by ‘delta` rows and drop entries that have scrolled entirely off-screen.
- #show_cursor ⇒ Object
- #soft_reset ⇒ Object
- #switch_to_alt_screen ⇒ Object
- #switch_to_main_screen ⇒ Object
- #tab ⇒ Object
-
#text_for_command_output(mark) ⇒ Object
Extract the visible text of a command’s output region.
- #to_text ⇒ Object
- #using_alt_screen? ⇒ Boolean
- #word_boundaries_at(row, col) ⇒ Object
Constructor Details
#initialize(rows: 24, cols: 80) ⇒ Screen
Returns a new instance of Screen.
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 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 |
# File 'lib/echoes/screen.rb', line 24 def initialize(rows: 24, cols: 80) @rows = rows @cols = cols @cursor = Cursor.new @attrs = Cell.new @grid = Array.new(rows) { Array.new(cols) { Cell.new } } @line_wrapped = Array.new(rows, false) @scroll_top = 0 @scroll_bottom = rows - 1 @saved_cursor = nil @scrollback = [] @scrollback_wrapped = [] @cell_pixel_width = 8.0 @cell_pixel_height = 16.0 @application_cursor_keys = false @bracketed_paste_mode = false @focus_reporting = false @sync_active = false # DEC private mode 2026 (synchronized output) @auto_wrap = true @mouse_tracking = :off # :off, :x10, :normal, :button_event, :any_event @mouse_encoding = :default # :default, :sgr @origin_mode = false @insert_mode = false @application_keypad = false @cursor_style = 0 # 0=default, 1=blinking block, 2=steady block, 3=blinking underline, 4=steady underline, 5=blinking bar, 6=steady bar @using_alt_screen = false @charset_g0 = :ascii # :ascii or :dec_special @charset_g1 = :ascii @charset_g2 = :ascii @charset_g3 = :ascii @active_charset = 0 # 0 = G0, 1 = G1 @single_shift = nil # nil, 2, or 3 (for SS2/SS3) @tab_stops = default_tab_stops @main_grid = nil @main_cursor = nil @main_scroll_top = nil @main_scroll_bottom = nil @main_saved_cursor = nil @main_scrollback = nil @pending_wrap = false @last_char = nil @title_stack = [] @placements = [] @dirty_rows = Set.new((0...rows).to_a) @bg_fills = [] # OSC 7772 ;bg-fill regions; each: {rect:[r1,c1,r2,c2], color:[r,g,b,a]} # OSC 133 prompt-boundary markers: each entry is a Hash with # :prompt_start / :input_start / :output_start / :output_end / # :exit_code keys, where the row values are *visual* row indices — # `scrollback_size + grid_row` at the moment the marker was seen. # When scrollback shifts off the front, mark rows decrement so # they keep pointing at the same content; marks that fall before # the scrollback floor are dropped. @command_marks = [] @current_command_mark = nil end |
Instance Attribute Details
#active_charset ⇒ Object
Returns the value of attribute active_charset.
813 814 815 |
# File 'lib/echoes/screen.rb', line 813 def active_charset @active_charset end |
#application_keypad ⇒ Object
Returns the value of attribute application_keypad.
813 814 815 |
# File 'lib/echoes/screen.rb', line 813 def application_keypad @application_keypad end |
#background ⇒ Object
Returns the value of attribute background.
9 10 11 |
# File 'lib/echoes/screen.rb', line 9 def background @background end |
#bell ⇒ Object
Returns the value of attribute bell.
813 814 815 |
# File 'lib/echoes/screen.rb', line 813 def bell @bell end |
#bg_fills ⇒ Object
Returns the value of attribute bg_fills.
9 10 11 |
# File 'lib/echoes/screen.rb', line 9 def bg_fills @bg_fills end |
#capture_handler ⇒ Object
Returns the value of attribute capture_handler.
1002 1003 1004 |
# File 'lib/echoes/screen.rb', line 1002 def capture_handler @capture_handler end |
#cell_pixel_height ⇒ Object
Returns the value of attribute cell_pixel_height.
9 10 11 |
# File 'lib/echoes/screen.rb', line 9 def cell_pixel_height @cell_pixel_height end |
#cell_pixel_width ⇒ Object
Returns the value of attribute cell_pixel_width.
9 10 11 |
# File 'lib/echoes/screen.rb', line 9 def cell_pixel_width @cell_pixel_width end |
#clipboard_handler ⇒ Object
Returns the value of attribute clipboard_handler.
1002 1003 1004 |
# File 'lib/echoes/screen.rb', line 1002 def clipboard_handler @clipboard_handler end |
#cols ⇒ Object (readonly)
Returns the value of attribute cols.
7 8 9 |
# File 'lib/echoes/screen.rb', line 7 def cols @cols end |
#command_marks ⇒ Object (readonly)
Returns the value of attribute command_marks.
7 8 9 |
# File 'lib/echoes/screen.rb', line 7 def command_marks @command_marks end |
#current_directory ⇒ Object
Returns the value of attribute current_directory.
9 10 11 |
# File 'lib/echoes/screen.rb', line 9 def current_directory @current_directory end |
#cursor ⇒ Object (readonly)
Returns the value of attribute cursor.
7 8 9 |
# File 'lib/echoes/screen.rb', line 7 def cursor @cursor end |
#cursor_style ⇒ Object
Returns the value of attribute cursor_style.
813 814 815 |
# File 'lib/echoes/screen.rb', line 813 def cursor_style @cursor_style end |
#dirty_rows ⇒ Object (readonly)
Returns the value of attribute dirty_rows.
7 8 9 |
# File 'lib/echoes/screen.rb', line 7 def dirty_rows @dirty_rows end |
#display_info_handler ⇒ Object
Returns the value of attribute display_info_handler.
1002 1003 1004 |
# File 'lib/echoes/screen.rb', line 1002 def display_info_handler @display_info_handler end |
#glyph_measurer ⇒ Object
Returns the value of attribute glyph_measurer.
1002 1003 1004 |
# File 'lib/echoes/screen.rb', line 1002 def glyph_measurer @glyph_measurer end |
#grid ⇒ Object (readonly)
Returns the value of attribute grid.
7 8 9 |
# File 'lib/echoes/screen.rb', line 7 def grid @grid end |
#insert_mode ⇒ Object
Returns the value of attribute insert_mode.
813 814 815 |
# File 'lib/echoes/screen.rb', line 813 def insert_mode @insert_mode end |
#mouse_encoding ⇒ Object
Returns the value of attribute mouse_encoding.
813 814 815 |
# File 'lib/echoes/screen.rb', line 813 def mouse_encoding @mouse_encoding end |
#mouse_tracking ⇒ Object
Returns the value of attribute mouse_tracking.
813 814 815 |
# File 'lib/echoes/screen.rb', line 813 def mouse_tracking @mouse_tracking end |
#notification_handler ⇒ Object
Returns the value of attribute notification_handler.
1002 1003 1004 |
# File 'lib/echoes/screen.rb', line 1002 def notification_handler @notification_handler end |
#open_window_handler ⇒ Object
Returns the value of attribute open_window_handler.
1002 1003 1004 |
# File 'lib/echoes/screen.rb', line 1002 def open_window_handler @open_window_handler end |
#palette_handler ⇒ Object
Returns the value of attribute palette_handler.
1002 1003 1004 |
# File 'lib/echoes/screen.rb', line 1002 def palette_handler @palette_handler end |
#pending_wrap ⇒ Object
Returns the value of attribute pending_wrap.
9 10 11 |
# File 'lib/echoes/screen.rb', line 9 def pending_wrap @pending_wrap end |
#placements ⇒ Object (readonly)
Tracks what kitty-graphics images are currently visible on this pane’s grid. Each entry: anchor_row:, anchor_col:, cell_cols:, cell_rows:, x_off:, y_off:, image:. image: is the width:, height: hash put_kitty_image received (held by reference, not a deep copy — the parser’s bitmap cache and the placement share the same object so a later a=d ;d=I … can delete by image id without re-decoding).
18 19 20 |
# File 'lib/echoes/screen.rb', line 18 def placements @placements end |
#rows ⇒ Object (readonly)
Returns the value of attribute rows.
7 8 9 |
# File 'lib/echoes/screen.rb', line 7 def rows @rows end |
#scrollback ⇒ Object (readonly)
Returns the value of attribute scrollback.
7 8 9 |
# File 'lib/echoes/screen.rb', line 7 def scrollback @scrollback end |
#single_shift ⇒ Object
Returns the value of attribute single_shift.
813 814 815 |
# File 'lib/echoes/screen.rb', line 813 def single_shift @single_shift end |
#sync_active ⇒ Object
Returns the value of attribute sync_active.
813 814 815 |
# File 'lib/echoes/screen.rb', line 813 def sync_active @sync_active end |
#title ⇒ Object
Returns the value of attribute title.
9 10 11 |
# File 'lib/echoes/screen.rb', line 9 def title @title end |
Class Method Details
Instance Method Details
#adjust_command_marks(delta) ⇒ Object
When scrollback shifts (oldest row dropped), every row index in now point before the scrollback floor are dropped — their content is no longer reachable.
987 988 989 990 991 992 993 994 995 996 997 998 999 1000 |
# File 'lib/echoes/screen.rb', line 987 def adjust_command_marks(delta) return if @command_marks.empty? @command_marks.each do |m| m.each_key do |k| next if k == :exit_code v = m[k] m[k] = v + delta if v end end @command_marks.reject! { |m| m[:prompt_start] && m[:prompt_start] < 0 } if @current_command_mark && (@current_command_mark[:prompt_start] || 0) < 0 @current_command_mark = nil end end |
#application_cursor_keys=(val) ⇒ Object
784 785 786 |
# File 'lib/echoes/screen.rb', line 784 def application_cursor_keys=(val) @application_cursor_keys = val end |
#application_cursor_keys? ⇒ Boolean
780 781 782 |
# File 'lib/echoes/screen.rb', line 780 def application_cursor_keys? @application_cursor_keys end |
#apply_sgr_subparams(sub) ⇒ Object
711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 |
# File 'lib/echoes/screen.rb', line 711 def apply_sgr_subparams(sub) case sub[0] when 4 # Underline style: 4:0=off, 4:1=single, 4:2=double, 4:3=curly, 4:4=dotted, 4:5=dashed style = sub[1] || 1 if style == 0 @attrs.underline = false else @attrs.underline = style end when 38 # Foreground color with sub-parameters if sub[1] == 2 # 38:2:cs:R:G:B or 38:2:R:G:B (cs = color space, often empty/omitted) r, g, b = extract_rgb_subparams(sub, 2) @attrs.fg = [r, g, b] if r && g && b elsif sub[1] == 5 && sub[2] @attrs.fg = sub[2] end when 48 # Background color with sub-parameters if sub[1] == 2 r, g, b = extract_rgb_subparams(sub, 2) @attrs.bg = [r, g, b] if r && g && b elsif sub[1] == 5 && sub[2] @attrs.bg = sub[2] end when 58 # Underline color if sub[1] == 2 r, g, b = extract_rgb_subparams(sub, 2) @attrs.underline_color = [r, g, b] if r && g && b elsif sub[1] == 5 && sub[2] @attrs.underline_color = sub[2] end when 59 @attrs.underline_color = nil end end |
#auto_wrap=(val) ⇒ Object
808 809 810 811 |
# File 'lib/echoes/screen.rb', line 808 def auto_wrap=(val) @auto_wrap = val @pending_wrap = false end |
#auto_wrap? ⇒ Boolean
804 805 806 |
# File 'lib/echoes/screen.rb', line 804 def auto_wrap? @auto_wrap end |
#backspace ⇒ Object
487 488 489 490 |
# File 'lib/echoes/screen.rb', line 487 def backspace @pending_wrap = false @cursor.col = [0, @cursor.col - 1].max end |
#backward_tab(n = 1) ⇒ Object
465 466 467 468 469 470 471 |
# File 'lib/echoes/screen.rb', line 465 def backward_tab(n = 1) @pending_wrap = false n.times do prev_stop = @tab_stops.reverse.find { |s| s < @cursor.col } @cursor.col = prev_stop || 0 end end |
#bracketed_paste_mode=(val) ⇒ Object
792 793 794 |
# File 'lib/echoes/screen.rb', line 792 def bracketed_paste_mode=(val) @bracketed_paste_mode = val end |
#bracketed_paste_mode? ⇒ Boolean
788 789 790 |
# File 'lib/echoes/screen.rb', line 788 def bracketed_paste_mode? @bracketed_paste_mode end |
#carriage_return ⇒ Object
436 437 438 439 |
# File 'lib/echoes/screen.rb', line 436 def carriage_return @pending_wrap = false @cursor.col = 0 end |
#clear_dirty ⇒ Object
831 832 833 |
# File 'lib/echoes/screen.rb', line 831 def clear_dirty @dirty_rows = Set.new end |
#clear_tab_stop(mode = 0) ⇒ Object
478 479 480 481 482 483 484 485 |
# File 'lib/echoes/screen.rb', line 478 def clear_tab_stop(mode = 0) case mode when 0 @tab_stops.delete(@cursor.col) when 3 @tab_stops.clear end end |
#clipboard_content ⇒ Object
1010 1011 1012 |
# File 'lib/echoes/screen.rb', line 1010 def clipboard_content @clipboard_handler&.call(:get, nil) end |
#decaln ⇒ Object
1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 |
# File 'lib/echoes/screen.rb', line 1169 def decaln @grid.each do |row| row.each do |cell| cell.reset! cell.char = 'E' end end @cursor.row = 0 @cursor.col = 0 @pending_wrap = false mark_all_dirty end |
#delete_chars(n = 1) ⇒ Object
551 552 553 554 555 556 557 558 559 |
# File 'lib/echoes/screen.rb', line 551 def delete_chars(n = 1) @pending_wrap = false row = @grid[@cursor.row] n.times do row.delete_at(@cursor.col) row.push(Cell.new) end mark_dirty(@cursor.row) end |
#delete_lines(n = 1) ⇒ Object
538 539 540 541 542 543 544 545 546 547 548 549 |
# File 'lib/echoes/screen.rb', line 538 def delete_lines(n = 1) @pending_wrap = false return unless @cursor.row >= @scroll_top && @cursor.row <= @scroll_bottom n.times do @grid.delete_at(@cursor.row) @line_wrapped.delete_at(@cursor.row) @grid.insert(@scroll_bottom, Array.new(@cols) { Cell.new }) @line_wrapped.insert(@scroll_bottom, false) end (@cursor.row..@scroll_bottom).each { |r| mark_dirty(r) } end |
#designate_charset(g, charset) ⇒ Object
1014 1015 1016 1017 1018 1019 1020 1021 |
# File 'lib/echoes/screen.rb', line 1014 def designate_charset(g, charset) case g when 0 then @charset_g0 = charset when 1 then @charset_g1 = charset when 2 then @charset_g2 = charset when 3 then @charset_g3 = charset end end |
#erase_chars(n = 1) ⇒ Object
571 572 573 574 575 576 577 578 |
# File 'lib/echoes/screen.rb', line 571 def erase_chars(n = 1) n.times do |i| col = @cursor.col + i break if col >= @cols @grid[@cursor.row][col].reset! end mark_dirty(@cursor.row) end |
#erase_in_display(mode = 0) ⇒ Object
492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 |
# File 'lib/echoes/screen.rb', line 492 def erase_in_display(mode = 0) @pending_wrap = false case mode when 0 @line_wrapped[@cursor.row] = false erase_in_line(0) ((@cursor.row + 1)...@rows).each { |r| clear_row(r); @line_wrapped[r] = false; mark_dirty(r) } when 1 erase_in_line(1) (0...@cursor.row).each { |r| clear_row(r); @line_wrapped[r] = false; mark_dirty(r) } when 2 (0...@rows).each { |r| clear_row(r); @line_wrapped[r] = false } @placements.clear mark_all_dirty when 3 @scrollback.clear @scrollback_wrapped.clear end end |
#erase_in_line(mode = 0) ⇒ Object
512 513 514 515 516 517 518 519 520 521 522 523 |
# File 'lib/echoes/screen.rb', line 512 def erase_in_line(mode = 0) @pending_wrap = false mark_dirty(@cursor.row) case mode when 0 (@cursor.col...@cols).each { |c| @grid[@cursor.row][c].reset! } when 1 (0..@cursor.col).each { |c| @grid[@cursor.row][c].reset! } when 2 clear_row(@cursor.row) end end |
#find_command_mark_at_row(abs_row) ⇒ Object
Find the command mark whose prompt+input region covers ‘abs_row`, or nil if no mark covers that row. The region runs from :prompt_start (inclusive) up to :output_start (exclusive). If a command is still running and no :output_start has been recorded yet, the region is taken as just :prompt_start itself (one row).
962 963 964 965 966 967 968 |
# File 'lib/echoes/screen.rb', line 962 def find_command_mark_at_row(abs_row) @command_marks.reverse_each.find do |m| next false unless m[:prompt_start] upper = m[:output_start] || (m[:prompt_start] + 1) abs_row >= m[:prompt_start] && abs_row < upper end end |
#focus_reporting=(val) ⇒ Object
800 801 802 |
# File 'lib/echoes/screen.rb', line 800 def focus_reporting=(val) @focus_reporting = val end |
#focus_reporting? ⇒ Boolean
796 797 798 |
# File 'lib/echoes/screen.rb', line 796 def focus_reporting? @focus_reporting end |
#hide_cursor ⇒ Object
1112 1113 1114 |
# File 'lib/echoes/screen.rb', line 1112 def hide_cursor @cursor.visible = false end |
#insert_chars(n = 1) ⇒ Object
561 562 563 564 565 566 567 568 569 |
# File 'lib/echoes/screen.rb', line 561 def insert_chars(n = 1) @pending_wrap = false row = @grid[@cursor.row] n.times do row.pop row.insert(@cursor.col, Cell.new) end mark_dirty(@cursor.row) end |
#insert_lines(n = 1) ⇒ Object
525 526 527 528 529 530 531 532 533 534 535 536 |
# File 'lib/echoes/screen.rb', line 525 def insert_lines(n = 1) @pending_wrap = false return unless @cursor.row >= @scroll_top && @cursor.row <= @scroll_bottom n.times do @grid.insert(@cursor.row, Array.new(@cols) { Cell.new }) @line_wrapped.insert(@cursor.row, false) @grid.delete_at(@scroll_bottom + 1) @line_wrapped.delete_at(@scroll_bottom + 1) end (@cursor.row..@scroll_bottom).each { |r| mark_dirty(r) } end |
#last_completed_command_mark ⇒ Object
Most recently completed command mark (D was emitted). nil if no command has finished yet on this pane.
944 945 946 |
# File 'lib/echoes/screen.rb', line 944 def last_completed_command_mark @command_marks.reverse_each.find { |m| m[:output_end] } end |
#line_feed ⇒ Object
441 442 443 444 445 446 447 448 |
# File 'lib/echoes/screen.rb', line 441 def line_feed @pending_wrap = false if @cursor.row == @scroll_bottom scroll_up(1) else @cursor.row = [@cursor.row + 1, @rows - 1].min end end |
#mark_all_dirty ⇒ Object
827 828 829 |
# File 'lib/echoes/screen.rb', line 827 def mark_all_dirty @dirty_rows = Set.new((0...@rows).to_a) end |
#mark_dirty(row) ⇒ Object
823 824 825 |
# File 'lib/echoes/screen.rb', line 823 def mark_dirty(row) @dirty_rows << row end |
#move_cursor(row, col) ⇒ Object
390 391 392 393 394 395 396 397 398 |
# File 'lib/echoes/screen.rb', line 390 def move_cursor(row, col) @pending_wrap = false if @origin_mode @cursor.row = (row + @scroll_top).clamp(@scroll_top, @scroll_bottom) else @cursor.row = clamp_row(row) end @cursor.col = clamp_col(col) end |
#move_cursor_backward(n = 1) ⇒ Object
431 432 433 434 |
# File 'lib/echoes/screen.rb', line 431 def move_cursor_backward(n = 1) @pending_wrap = false @cursor.col = [0, @cursor.col - n].max end |
#move_cursor_down(n = 1) ⇒ Object
406 407 408 409 410 |
# File 'lib/echoes/screen.rb', line 406 def move_cursor_down(n = 1) @pending_wrap = false bottom = @cursor.row <= @scroll_bottom ? @scroll_bottom : @rows - 1 @cursor.row = [bottom, @cursor.row + n].min end |
#move_cursor_forward(n = 1) ⇒ Object
426 427 428 429 |
# File 'lib/echoes/screen.rb', line 426 def move_cursor_forward(n = 1) @pending_wrap = false @cursor.col = [@cols - 1, @cursor.col + n].min end |
#move_cursor_next_line(n = 1) ⇒ Object
412 413 414 415 416 417 |
# File 'lib/echoes/screen.rb', line 412 def move_cursor_next_line(n = 1) @pending_wrap = false bottom = @cursor.row <= @scroll_bottom ? @scroll_bottom : @rows - 1 @cursor.row = [bottom, @cursor.row + n].min @cursor.col = 0 end |
#move_cursor_prev_line(n = 1) ⇒ Object
419 420 421 422 423 424 |
# File 'lib/echoes/screen.rb', line 419 def move_cursor_prev_line(n = 1) @pending_wrap = false top = @cursor.row >= @scroll_top ? @scroll_top : 0 @cursor.row = [top, @cursor.row - n].max @cursor.col = 0 end |
#move_cursor_up(n = 1) ⇒ Object
400 401 402 403 404 |
# File 'lib/echoes/screen.rb', line 400 def move_cursor_up(n = 1) @pending_wrap = false top = @cursor.row >= @scroll_top ? @scroll_top : 0 @cursor.row = [top, @cursor.row - n].max end |
#origin_mode=(val) ⇒ Object
1027 1028 1029 1030 1031 1032 1033 1034 |
# File 'lib/echoes/screen.rb', line 1027 def origin_mode=(val) @origin_mode = val @pending_wrap = false if val @cursor.row = @scroll_top @cursor.col = 0 end end |
#origin_mode? ⇒ Boolean
1023 1024 1025 |
# File 'lib/echoes/screen.rb', line 1023 def origin_mode? @origin_mode end |
#osc133_mark(kind, exit_code: nil) ⇒ Object
OSC 133 prompt boundary marker. ‘kind` is one of:
:prompt_start — OSC 133 ; A — beginning of a fresh prompt block
:prompt_end — OSC 133 ; B — end of prompt / start of input
:command_start — OSC 133 ; C — start of command output
:command_end — OSC 133 ; D — end of command output (with optional exit code)
Marks are stored as visual rows (scrollback rows + grid rows from 0). ‘:prompt_start` opens a new mark; subsequent kinds populate it.
896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 |
# File 'lib/echoes/screen.rb', line 896 def osc133_mark(kind, exit_code: nil) row = @scrollback.size + @cursor.row case kind when :prompt_start @current_command_mark = { prompt_start: row, input_start: nil, output_start: nil, output_end: nil, exit_code: nil, } @command_marks << @current_command_mark when :prompt_end @current_command_mark ||= {prompt_start: row, input_start: nil, output_start: nil, output_end: nil, exit_code: nil} @command_marks << @current_command_mark unless @command_marks.last.equal?(@current_command_mark) @current_command_mark[:input_start] = row when :command_start return unless @current_command_mark @current_command_mark[:output_start] = row when :command_end return unless @current_command_mark @current_command_mark[:output_end] = row @current_command_mark[:exit_code] = exit_code end end |
#output_region_for_row(abs_row) ⇒ Object
Find the command mark whose output region covers ‘abs_row` and return [start_row, end_row] (both inclusive) so callers like the GUI’s triple-click can highlight or copy that whole region. nil if no completed mark covers the row.
974 975 976 977 978 979 980 981 |
# File 'lib/echoes/screen.rb', line 974 def output_region_for_row(abs_row) mark = @command_marks.reverse_each.find do |m| next false unless m[:output_start] && m[:output_end] abs_row >= m[:output_start] && abs_row < m[:output_end] end return nil unless mark [mark[:output_start], mark[:output_end] - 1] end |
#pop_title ⇒ Object
819 820 821 |
# File 'lib/echoes/screen.rb', line 819 def pop_title @title = @title_stack.pop if @title_stack.any? end |
#push_title ⇒ Object
815 816 817 |
# File 'lib/echoes/screen.rb', line 815 def push_title @title_stack.push(@title) end |
#put_char(c) ⇒ Object
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 |
# File 'lib/echoes/screen.rb', line 91 def put_char(c) if c.bytesize == 1 if @single_shift cs = @single_shift == 2 ? @charset_g2 : @charset_g3 @single_shift = nil else cs = @active_charset == 0 ? @charset_g0 : @charset_g1 end if cs == :dec_special c = DEC_SPECIAL.fetch(c, c) end end # Combining characters: append to previous cell if combining?(c) col = @pending_wrap ? @cursor.col : [0, @cursor.col - 1].max col -= 1 if col > 0 && @grid[@cursor.row][col].width == 0 @grid[@cursor.row][col].char += c @last_char = @grid[@cursor.row][col].char return end w = char_width(c) if @auto_wrap # Deferred wrap: if the previous character set the flag, wrap now if @pending_wrap @pending_wrap = false @line_wrapped[@cursor.row] = true @cursor.col = 0 line_feed end # Wide char at last column: doesn't fit, wrap first if w == 2 && @cursor.col == @cols - 1 @grid[@cursor.row][@cursor.col].reset! @line_wrapped[@cursor.row] = true @cursor.col = 0 line_feed end else # No wrap: clamp cursor to last column if w == 2 && @cursor.col >= @cols - 1 @cursor.col = @cols - 2 elsif @cursor.col >= @cols @cursor.col = @cols - 1 end end erase_multicell_at(@cursor.row, @cursor.col) if @insert_mode row = @grid[@cursor.row] w.times { row.pop; row.insert(@cursor.col, Cell.new) } end cell = @grid[@cursor.row][@cursor.col] cell.copy_from(@attrs) cell.char = c cell.width = w if w == 2 && @cursor.col + 1 < @cols # Mark the next cell as a continuation (width 0) next_cell = @grid[@cursor.row][@cursor.col + 1] next_cell.reset! next_cell.width = 0 end mark_dirty(@cursor.row) @cursor.col += w if @cursor.col >= @cols @cursor.col = @cols - 1 @pending_wrap = true if @auto_wrap end @last_char = c end |
#put_kitty_image(rgba:, width:, height:, cells_w: nil, cells_h: nil, px_x_offset: 0, px_y_offset: 0, suppress_cursor: false, image_id: nil) ⇒ Object
Place a Kitty-graphics-protocol image as a multicell anchor. ‘rgba` is a Ruby string of width*height*4 bytes (RGBA8, top row first). `cells_w` / `cells_h` come from the wire’s ‘c=` / `r=` options; when nil we size to the image’s natural pixel dims divided by cell pixel size. ‘suppress_cursor` honors the `C=1` request to leave the cursor where it was.
The renderer is format-agnostic: it just blits ‘multicell.sixel`’s RGBA into the reserved cell rect, so we reuse that storage key rather than introducing a parallel ‘:image` key.
236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 |
# File 'lib/echoes/screen.rb', line 236 def put_kitty_image(rgba:, width:, height:, cells_w: nil, cells_h: nil, px_x_offset: 0, px_y_offset: 0, suppress_cursor: false, image_id: nil) return if rgba.nil? || width <= 0 || height <= 0 return if @cell_pixel_width.to_f <= 0 || @cell_pixel_height.to_f <= 0 mc_cols = cells_w && cells_w > 0 ? cells_w : (width / @cell_pixel_width ).ceil mc_rows = cells_h && cells_h > 0 ? cells_h : (height / @cell_pixel_height).ceil mc_cols = [mc_cols, 1].max mc_rows = [mc_rows, 1].max return if mc_cols > @cols || mc_rows > @rows if suppress_cursor # C=1 (slide-presentation mode): anchor at the current # cursor without wrapping or scrolling. A multi-image # slideshow would otherwise accumulate a cumulative # scroll offset every time an image landed near the # bottom, dragging earlier rows off-screen. If the image # doesn't fit at the current position, bail — the client # positions the cursor deliberately and would rather see # nothing than have the layout shift out from under it. return if @cursor.col + mc_cols > @cols return if @cursor.row + mc_rows > @rows else if @cursor.col + mc_cols > @cols @cursor.col = 0 line_feed end while @cursor.row + mc_rows > @rows scroll_up(1) @cursor.row = [@cursor.row - 1, 0].max end end anchor_row = @cursor.row anchor_col = @cursor.col mc_rows.times do |dr| mc_cols.times do |dc| erase_multicell_at(anchor_row + dr, anchor_col + dc) end end anchor = @grid[anchor_row][anchor_col] anchor.reset! anchor.char = " " anchor.width = 1 # Multicell anchor reserves the cell rect (cursor flow, # `:cont` neighbors stay skipped by the renderer); the # actual image is drawn by the GUI's placement re-blit # pass off `screen.placements`, not via mc[:sixel]. Keeps # the kitty path independent of the cell loop so deletes, # scrolls, and font changes affect drawing through one # code path. anchor.multicell = { cols: mc_cols, rows: mc_rows, scale: 1, frac_n: 0, frac_d: 0, valign: 0, halign: 0, } mc_rows.times do |dr| mc_cols.times do |dc| next if dr == 0 && dc == 0 cont = @grid[anchor_row + dr][anchor_col + dc] cont.reset! cont.multicell = :cont end end # Record the placement BEFORE any post-place scroll fires — # if the cursor advance below pushes us past the bottom and # triggers scroll_up, that path needs the placement already # in @placements so its anchor_row gets decremented along # with the rest of the content. @placements << { image_id: image_id, anchor_row: anchor_row, anchor_col: anchor_col, cell_cols: mc_cols, cell_rows: mc_rows, x_off: px_x_offset.to_i, y_off: px_y_offset.to_i, image: {rgba: rgba, width: width, height: height}, } unless suppress_cursor # Sixel parity: cursor lands at column 0 of the row after # the image. If that row is past the bottom, scroll. @cursor.col = 0 @cursor.row += mc_rows while @cursor.row >= @rows scroll_up(1) @cursor.row -= 1 end end mark_all_dirty end |
#put_multicell(text, scale:, width:, frac_n:, frac_d:, valign:, halign:, family: nil, flip_h: false, flip_v: false) ⇒ Object
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 |
# File 'lib/echoes/screen.rb', line 176 def put_multicell(text, scale:, width:, frac_n:, frac_d:, valign:, halign:, family: nil, flip_h: false, flip_v: false) mc_rows = scale if width > 0 # Explicit width: entire text in one block of scale*width cols × scale rows place_multicell_block(text, scale * width, mc_rows, scale, frac_n, frac_d, valign, halign, family, flip_h, flip_v) elsif halign != 0 # h= is set: render the whole string as one block of # `scale × source_chars` cells, so the renderer's halign math # has room to center / right-align the glyphs. The spec only # mandates h= when the glyphs are smaller than the block # (fractional n<d), but extending it to non-fractional / # proportional text is a natural superset — other terminals # just ignore the attribute. With a proportional family we # widen the block to `max(scale × source_chars, measured)` so # the text never overflows but still gets visible side # margins for centering. source_chars = text.each_grapheme_cluster.sum { |g| char_width(g) } mc_cols = scale * source_chars if family && @glyph_measurer && @cell_pixel_width && @cell_pixel_width > 0 measured_px = @glyph_measurer.call(text, family, scale, frac_n, frac_d).to_f measured_cells = (measured_px / @cell_pixel_width).ceil mc_cols = [mc_cols, measured_cells].max end mc_cols = [mc_cols, 1].max place_multicell_block(text, mc_cols, mc_rows, scale, frac_n, frac_d, valign, halign, family, flip_h, flip_v) elsif family && @glyph_measurer && @cell_pixel_width && @cell_pixel_width > 0 # Proportional fonts have variable glyph widths, so reserving # `char_width(grapheme) * scale` cells per grapheme leaves big # letters (Noto Serif "H" at 2×) overflowing into the next # cell and small letters ("l") under-filling theirs. Ask the # host to measure the whole text in the requested font and # reserve enough cells for the entire block, drawn as one # unit by the renderer's existing string-draw path. measured_px = @glyph_measurer.call(text, family, scale, frac_n, frac_d).to_f mc_cols = (measured_px / @cell_pixel_width).ceil mc_cols = [mc_cols, 1].max place_multicell_block(text, mc_cols, mc_rows, scale, frac_n, frac_d, valign, halign, family, flip_h, flip_v) else # Auto width: each grapheme gets its own block (monospace # assumption — fine for the configured terminal font). text.each_grapheme_cluster do |grapheme| cw = char_width(grapheme) mc_cols = scale * cw place_multicell_block(grapheme, mc_cols, mc_rows, scale, frac_n, frac_d, valign, halign, family, flip_h, flip_v) end end end |
#put_sixel(data, params) ⇒ Object
334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 |
# File 'lib/echoes/screen.rb', line 334 def put_sixel(data, params) decoder = SixelDecoder.new(params).decode(data) return if decoder.width == 0 || decoder.height == 0 mc_cols = (decoder.width / @cell_pixel_width).ceil mc_rows = (decoder.height / @cell_pixel_height).ceil return if mc_cols > @cols || mc_rows > @rows # Wrap if it doesn't fit on current line if @cursor.col + mc_cols > @cols @cursor.col = 0 line_feed end # Scroll if block doesn't fit vertically while @cursor.row + mc_rows > @rows scroll_up(1) @cursor.row = [@cursor.row - 1, 0].max end anchor_row = @cursor.row anchor_col = @cursor.col # Erase existing cells in the block area mc_rows.times do |dr| mc_cols.times do |dc| erase_multicell_at(anchor_row + dr, anchor_col + dc) end end # Set anchor cell with sixel data anchor = @grid[anchor_row][anchor_col] anchor.reset! anchor.char = " " anchor.width = 1 anchor.multicell = { cols: mc_cols, rows: mc_rows, scale: 1, frac_n: 0, frac_d: 0, valign: 0, halign: 0, sixel: { width: decoder.width, height: decoder.height, rgba: decoder.to_rgba } } # Mark continuation cells mc_rows.times do |dr| mc_cols.times do |dc| next if dr == 0 && dc == 0 cont = @grid[anchor_row + dr][anchor_col + dc] cont.reset! cont.multicell = :cont end end @cursor.col = 0 @cursor.row = [anchor_row + mc_rows, @rows - 1].min end |
#put_styled_segments(segments) ⇒ Object
Write a sequence of styled prompt segments directly into the cell grid, bypassing the ANSI SGR parser. Each segment is a ‘fg:, bg:, bold:, italic:, underline:, inverse:` Hash (see `Rubish::REPL#prompt_segments`). Color values follow rubish’s encoding: nil = default, 0..255 = palette index, ‘[:rgb, r, g, b]` = true color (translated to `[r, g, b]` for this Screen’s storage convention).
Existing ‘@attrs` is snapshotted and restored so any in-flight SGR state from prior parser-driven rendering is preserved.
849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 |
# File 'lib/echoes/screen.rb', line 849 def put_styled_segments(segments) saved_fg = @attrs.fg saved_bg = @attrs.bg saved_bold = @attrs.bold saved_italic = @attrs.italic saved_underline = @attrs.underline saved_inverse = @attrs.inverse begin segments.each do |seg| @attrs.fg = translate_segment_color(seg[:fg]) @attrs.bg = translate_segment_color(seg[:bg]) @attrs.bold = !!seg[:bold] @attrs.italic = !!seg[:italic] @attrs.underline = !!seg[:underline] @attrs.inverse = !!seg[:inverse] (seg[:text] || '').each_char { |c| put_char(c) } end ensure @attrs.fg = saved_fg @attrs.bg = saved_bg @attrs.bold = saved_bold @attrs.italic = saved_italic @attrs.underline = saved_underline @attrs.inverse = saved_inverse end end |
#repeat_char(n = 1) ⇒ Object
170 171 172 173 174 |
# File 'lib/echoes/screen.rb', line 170 def repeat_char(n = 1) return unless @last_char n.times { put_char(@last_char) } end |
#reset ⇒ Object
1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 |
# File 'lib/echoes/screen.rb', line 1182 def reset @cursor = Cursor.new @attrs = Cell.new @grid = Array.new(@rows) { Array.new(@cols) { Cell.new } } @line_wrapped = Array.new(@rows, false) @scroll_top = 0 @scroll_bottom = @rows - 1 @saved_cursor = nil @scrollback = [] @scrollback_wrapped = [] @tab_stops = default_tab_stops @pending_wrap = false @placements = [] mark_all_dirty end |
#resize(new_rows, new_cols) ⇒ Object
1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 |
# File 'lib/echoes/screen.rb', line 1198 def resize(new_rows, new_cols) old_cols = @cols @rows = new_rows @cols = new_cols if new_cols != old_cols reflow(new_rows, new_cols, old_cols) else # Only row count changed — simple add/remove if new_rows > @grid.size (new_rows - @grid.size).times do @grid.push(Array.new(new_cols) { Cell.new }) @line_wrapped.push(false) end elsif new_rows < @grid.size @grid.slice!(new_rows..) @line_wrapped.slice!(new_rows..) end end @scroll_top = 0 @scroll_bottom = new_rows - 1 @cursor.row = clamp_row(@cursor.row) @cursor.col = clamp_col(@cursor.col) @pending_wrap = false end |
#restore_cursor ⇒ Object
766 767 768 769 770 771 772 773 774 775 776 777 778 |
# File 'lib/echoes/screen.rb', line 766 def restore_cursor if @saved_cursor @cursor.row = @saved_cursor[:row] @cursor.col = @saved_cursor[:col] @attrs.copy_from(@saved_cursor[:attrs]) @origin_mode = @saved_cursor[:origin_mode] @auto_wrap = @saved_cursor[:auto_wrap] @charset_g0 = @saved_cursor[:charset_g0] @charset_g1 = @saved_cursor[:charset_g1] @active_charset = @saved_cursor[:active_charset] @pending_wrap = @saved_cursor[:pending_wrap] || false end end |
#reverse_index ⇒ Object
450 451 452 453 454 455 456 457 |
# File 'lib/echoes/screen.rb', line 450 def reverse_index @pending_wrap = false if @cursor.row == @scroll_top scroll_down(1) else @cursor.row = [0, @cursor.row - 1].max end end |
#save_cursor ⇒ Object
751 752 753 754 755 756 757 758 759 760 761 762 763 764 |
# File 'lib/echoes/screen.rb', line 751 def save_cursor saved_attrs = Cell.new saved_attrs.copy_from(@attrs) @saved_cursor = { row: @cursor.row, col: @cursor.col, attrs: saved_attrs, origin_mode: @origin_mode, auto_wrap: @auto_wrap, charset_g0: @charset_g0, charset_g1: @charset_g1, active_charset: @active_charset, pending_wrap: @pending_wrap, } end |
#scroll_down(n = 1) ⇒ Object
612 613 614 615 616 617 618 619 620 621 |
# File 'lib/echoes/screen.rb', line 612 def scroll_down(n = 1) @pending_wrap = false n.times do @grid.delete_at(@scroll_bottom) @line_wrapped.delete_at(@scroll_bottom) @grid.insert(@scroll_top, Array.new(@cols) { Cell.new }) @line_wrapped.insert(@scroll_top, false) end (@scroll_top..@scroll_bottom).each { |r| mark_dirty(r) } end |
#scroll_up(n = 1) ⇒ Object
580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 |
# File 'lib/echoes/screen.rb', line 580 def scroll_up(n = 1) @pending_wrap = false n.times do if @scroll_top == 0 row = @grid[@scroll_top] @scrollback << row.map { |cell| c = Cell.new; c.copy_from(cell); c.width = cell.width; c.multicell = cell.multicell; c } @scrollback_wrapped << @line_wrapped[@scroll_top] if @scrollback.size > self.class.scrollback_limit @scrollback.shift adjust_command_marks(-1) end @scrollback_wrapped.shift if @scrollback_wrapped.size > self.class.scrollback_limit end @grid.delete_at(@scroll_top) @line_wrapped.delete_at(@scroll_top) @grid.insert(@scroll_bottom, Array.new(@cols) { Cell.new }) @line_wrapped.insert(@scroll_bottom, false) end shift_placements(-n) (@scroll_top..@scroll_bottom).each { |r| mark_dirty(r) } end |
#selected_text(sr, sc, er, ec) ⇒ Object
1120 1121 1122 1123 1124 1125 1126 1127 1128 |
# File 'lib/echoes/screen.rb', line 1120 def selected_text(sr, sc, er, ec) lines = [] (sr..er).each do |r| from = (r == sr) ? sc : 0 to = (r == er) ? ec : @cols - 1 lines << @grid[r][from..to].map { |cell| cell.char }.join.rstrip end lines.join("\n") end |
#set_clipboard(text) ⇒ Object
1006 1007 1008 |
# File 'lib/echoes/screen.rb', line 1006 def set_clipboard(text) @clipboard_handler&.call(:set, text) end |
#set_current_command_text(text) ⇒ Object
Attach the literal command text to the most recently opened mark. The host calls this at submit time (between OSC 133 ;B and ;C) so click-to-rerun can recover the command text from a clicked prompt row long after submission.
952 953 954 955 |
# File 'lib/echoes/screen.rb', line 952 def set_current_command_text(text) return unless @current_command_mark @current_command_mark[:command_text] = text end |
#set_graphics(params) ⇒ Object
631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 |
# File 'lib/echoes/screen.rb', line 631 def set_graphics(params) params = [0] if params.empty? i = 0 while i < params.length p = params[i] # Handle colon sub-parameter arrays (e.g. [38, 2, nil, R, G, B]) if p.is_a?(Array) apply_sgr_subparams(p) i += 1 next end case p when 0, nil @attrs.reset! when 1 @attrs.bold = true when 2 @attrs.faint = true when 3 @attrs.italic = true when 4 @attrs.underline = true when 7 @attrs.inverse = true when 5, 6 @attrs.blink = true when 8 @attrs.concealed = true when 9 @attrs.strikethrough = true when 22 @attrs.bold = false @attrs.faint = false when 23 @attrs.italic = false when 24 @attrs.underline = false when 27 @attrs.inverse = false when 25 @attrs.blink = false when 28 @attrs.concealed = false when 29 @attrs.strikethrough = false when 30..37 @attrs.fg = p - 30 when 38 if params[i + 1] == 2 && params[i + 2] && params[i + 3] && params[i + 4] @attrs.fg = [params[i + 2], params[i + 3], params[i + 4]] i += 4 elsif params[i + 1] == 5 && params[i + 2] @attrs.fg = params[i + 2] i += 2 end when 39 @attrs.fg = nil when 40..47 @attrs.bg = p - 40 when 48 if params[i + 1] == 2 && params[i + 2] && params[i + 3] && params[i + 4] @attrs.bg = [params[i + 2], params[i + 3], params[i + 4]] i += 4 elsif params[i + 1] == 5 && params[i + 2] @attrs.bg = params[i + 2] i += 2 end when 49 @attrs.bg = nil when 90..97 @attrs.fg = p - 90 + 8 when 100..107 @attrs.bg = p - 100 + 8 end i += 1 end end |
#set_hyperlink(uri) ⇒ Object
835 836 837 |
# File 'lib/echoes/screen.rb', line 835 def set_hyperlink(uri) @attrs.hyperlink = uri end |
#set_scroll_region(top, bottom) ⇒ Object
623 624 625 626 627 628 629 |
# File 'lib/echoes/screen.rb', line 623 def set_scroll_region(top, bottom) @pending_wrap = false @scroll_top = clamp_row(top) @scroll_bottom = clamp_row(bottom) @cursor.row = 0 @cursor.col = 0 end |
#set_tab_stop ⇒ Object
473 474 475 476 |
# File 'lib/echoes/screen.rb', line 473 def set_tab_stop @tab_stops << @cursor.col unless @tab_stops.include?(@cursor.col) @tab_stops.sort! end |
#shift_placements(delta) ⇒ Object
Move every placement anchor by ‘delta` rows and drop entries that have scrolled entirely off-screen. Called by scroll_up so the placement list tracks the same visual shift the grid rows just took.
606 607 608 609 610 |
# File 'lib/echoes/screen.rb', line 606 def shift_placements(delta) return if @placements.empty? @placements.each { |p| p[:anchor_row] += delta } @placements.reject! { |p| p[:anchor_row] + p[:cell_rows] <= 0 } end |
#show_cursor ⇒ Object
1108 1109 1110 |
# File 'lib/echoes/screen.rb', line 1108 def show_cursor @cursor.visible = true end |
#soft_reset ⇒ Object
1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 |
# File 'lib/echoes/screen.rb', line 1145 def soft_reset @attrs = Cell.new @cursor.visible = true @saved_cursor = nil @origin_mode = false @auto_wrap = true @insert_mode = false @application_cursor_keys = false @bracketed_paste_mode = false @focus_reporting = false @sync_active = false # DEC private mode 2026 (synchronized output) @charset_g0 = :ascii @charset_g1 = :ascii @charset_g2 = :ascii @charset_g3 = :ascii @active_charset = 0 @single_shift = nil @cursor_style = 0 @tab_stops = default_tab_stops @scroll_top = 0 @scroll_bottom = @rows - 1 @pending_wrap = false end |
#switch_to_alt_screen ⇒ Object
1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 |
# File 'lib/echoes/screen.rb', line 1040 def switch_to_alt_screen return if @using_alt_screen @main_grid = @grid @main_line_wrapped = @line_wrapped @main_cursor = [@cursor.row, @cursor.col, @cursor.visible] @main_scroll_top = @scroll_top @main_scroll_bottom = @scroll_bottom @main_saved_cursor = @saved_cursor @main_scrollback = @scrollback @main_scrollback_wrapped = @scrollback_wrapped @main_rows = @rows @main_cols = @cols @grid = Array.new(@rows) { Array.new(@cols) { Cell.new } } @line_wrapped = Array.new(@rows, false) @cursor = Cursor.new @attrs = Cell.new @scroll_top = 0 @scroll_bottom = @rows - 1 @saved_cursor = nil @scrollback = [] @scrollback_wrapped = [] @pending_wrap = false @using_alt_screen = true mark_all_dirty end |
#switch_to_main_screen ⇒ Object
1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 |
# File 'lib/echoes/screen.rb', line 1068 def switch_to_main_screen return unless @using_alt_screen current_rows = @rows current_cols = @cols @grid = @main_grid @line_wrapped = @main_line_wrapped @cursor = Cursor.new @cursor.row, @cursor.col, @cursor.visible = @main_cursor @scroll_top = @main_scroll_top @scroll_bottom = @main_scroll_bottom @saved_cursor = @main_saved_cursor @scrollback = @main_scrollback @scrollback_wrapped = @main_scrollback_wrapped @rows = @main_rows @cols = @main_cols @attrs = Cell.new @main_grid = nil @main_line_wrapped = nil @main_cursor = nil @main_scroll_top = nil @main_scroll_bottom = nil @main_saved_cursor = nil @main_scrollback = nil @main_scrollback_wrapped = nil @main_rows = nil @main_cols = nil @pending_wrap = false @using_alt_screen = false # If terminal was resized while in alt screen, adjust the restored main grid if current_rows != @rows || current_cols != @cols resize(current_rows, current_cols) end mark_all_dirty end |
#tab ⇒ Object
459 460 461 462 463 |
# File 'lib/echoes/screen.rb', line 459 def tab @pending_wrap = false next_stop = @tab_stops.find { |s| s > @cursor.col } @cursor.col = next_stop ? [next_stop, @cols - 1].min : @cols - 1 end |
#text_for_command_output(mark) ⇒ Object
Extract the visible text of a command’s output region. ‘mark` is one of the entries from `@command_marks`. Rows that have scrolled off the front of the scrollback are silently skipped — the text is no longer recoverable. Returns “” when the mark is incomplete (no :output_start or :output_end yet).
925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 |
# File 'lib/echoes/screen.rb', line 925 def text_for_command_output(mark) return '' unless mark && mark[:output_start] && mark[:output_end] from = mark[:output_start] to = mark[:output_end] return '' if to <= from sb_size = @scrollback.size lines = [] (from...to).each do |abs_row| row = abs_row < 0 ? nil : abs_row < sb_size ? @scrollback[abs_row] : @grid[abs_row - sb_size] next unless row lines << row.map { |c| c.char || ' ' }.join.rstrip end lines.join("\n") end |
#to_text ⇒ Object
1116 1117 1118 |
# File 'lib/echoes/screen.rb', line 1116 def to_text @grid.map { |row| row.map { |cell| cell.char }.join.rstrip }.join("\n").rstrip end |
#using_alt_screen? ⇒ Boolean
1036 1037 1038 |
# File 'lib/echoes/screen.rb', line 1036 def using_alt_screen? @using_alt_screen end |
#word_boundaries_at(row, col) ⇒ Object
1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 |
# File 'lib/echoes/screen.rb', line 1130 def word_boundaries_at(row, col) return nil if row < 0 || row >= @rows || col < 0 || col >= @cols line = @grid[row] cls = char_class(line[col].char) start_col = col start_col -= 1 while start_col > 0 && char_class(line[start_col - 1].char) == cls end_col = col end_col += 1 while end_col < @cols - 1 && char_class(line[end_col + 1].char) == cls [start_col, end_col] end |