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, z_index: 0) ⇒ 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.
828 829 830 |
# File 'lib/echoes/screen.rb', line 828 def active_charset @active_charset end |
#application_keypad ⇒ Object
Returns the value of attribute application_keypad.
828 829 830 |
# File 'lib/echoes/screen.rb', line 828 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.
828 829 830 |
# File 'lib/echoes/screen.rb', line 828 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.
1017 1018 1019 |
# File 'lib/echoes/screen.rb', line 1017 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.
1017 1018 1019 |
# File 'lib/echoes/screen.rb', line 1017 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.
828 829 830 |
# File 'lib/echoes/screen.rb', line 828 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.
1017 1018 1019 |
# File 'lib/echoes/screen.rb', line 1017 def display_info_handler @display_info_handler end |
#glyph_measurer ⇒ Object
Returns the value of attribute glyph_measurer.
1017 1018 1019 |
# File 'lib/echoes/screen.rb', line 1017 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.
828 829 830 |
# File 'lib/echoes/screen.rb', line 828 def insert_mode @insert_mode end |
#mouse_encoding ⇒ Object
Returns the value of attribute mouse_encoding.
828 829 830 |
# File 'lib/echoes/screen.rb', line 828 def mouse_encoding @mouse_encoding end |
#mouse_tracking ⇒ Object
Returns the value of attribute mouse_tracking.
828 829 830 |
# File 'lib/echoes/screen.rb', line 828 def mouse_tracking @mouse_tracking end |
#notification_handler ⇒ Object
Returns the value of attribute notification_handler.
1017 1018 1019 |
# File 'lib/echoes/screen.rb', line 1017 def notification_handler @notification_handler end |
#open_window_handler ⇒ Object
Returns the value of attribute open_window_handler.
1017 1018 1019 |
# File 'lib/echoes/screen.rb', line 1017 def open_window_handler @open_window_handler end |
#palette_handler ⇒ Object
Returns the value of attribute palette_handler.
1017 1018 1019 |
# File 'lib/echoes/screen.rb', line 1017 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.
828 829 830 |
# File 'lib/echoes/screen.rb', line 828 def single_shift @single_shift end |
#sync_active ⇒ Object
Returns the value of attribute sync_active.
828 829 830 |
# File 'lib/echoes/screen.rb', line 828 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.
1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 |
# File 'lib/echoes/screen.rb', line 1002 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
799 800 801 |
# File 'lib/echoes/screen.rb', line 799 def application_cursor_keys=(val) @application_cursor_keys = val end |
#application_cursor_keys? ⇒ Boolean
795 796 797 |
# File 'lib/echoes/screen.rb', line 795 def application_cursor_keys? @application_cursor_keys end |
#apply_sgr_subparams(sub) ⇒ Object
726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 |
# File 'lib/echoes/screen.rb', line 726 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
823 824 825 826 |
# File 'lib/echoes/screen.rb', line 823 def auto_wrap=(val) @auto_wrap = val @pending_wrap = false end |
#auto_wrap? ⇒ Boolean
819 820 821 |
# File 'lib/echoes/screen.rb', line 819 def auto_wrap? @auto_wrap end |
#backspace ⇒ Object
502 503 504 505 |
# File 'lib/echoes/screen.rb', line 502 def backspace @pending_wrap = false @cursor.col = [0, @cursor.col - 1].max end |
#backward_tab(n = 1) ⇒ Object
480 481 482 483 484 485 486 |
# File 'lib/echoes/screen.rb', line 480 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
807 808 809 |
# File 'lib/echoes/screen.rb', line 807 def bracketed_paste_mode=(val) @bracketed_paste_mode = val end |
#bracketed_paste_mode? ⇒ Boolean
803 804 805 |
# File 'lib/echoes/screen.rb', line 803 def bracketed_paste_mode? @bracketed_paste_mode end |
#carriage_return ⇒ Object
451 452 453 454 |
# File 'lib/echoes/screen.rb', line 451 def carriage_return @pending_wrap = false @cursor.col = 0 end |
#clear_dirty ⇒ Object
846 847 848 |
# File 'lib/echoes/screen.rb', line 846 def clear_dirty @dirty_rows = Set.new end |
#clear_tab_stop(mode = 0) ⇒ Object
493 494 495 496 497 498 499 500 |
# File 'lib/echoes/screen.rb', line 493 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
1025 1026 1027 |
# File 'lib/echoes/screen.rb', line 1025 def clipboard_content @clipboard_handler&.call(:get, nil) end |
#decaln ⇒ Object
1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 |
# File 'lib/echoes/screen.rb', line 1184 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
566 567 568 569 570 571 572 573 574 |
# File 'lib/echoes/screen.rb', line 566 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
553 554 555 556 557 558 559 560 561 562 563 564 |
# File 'lib/echoes/screen.rb', line 553 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
1029 1030 1031 1032 1033 1034 1035 1036 |
# File 'lib/echoes/screen.rb', line 1029 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
586 587 588 589 590 591 592 593 |
# File 'lib/echoes/screen.rb', line 586 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
507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 |
# File 'lib/echoes/screen.rb', line 507 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
527 528 529 530 531 532 533 534 535 536 537 538 |
# File 'lib/echoes/screen.rb', line 527 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).
977 978 979 980 981 982 983 |
# File 'lib/echoes/screen.rb', line 977 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
815 816 817 |
# File 'lib/echoes/screen.rb', line 815 def focus_reporting=(val) @focus_reporting = val end |
#focus_reporting? ⇒ Boolean
811 812 813 |
# File 'lib/echoes/screen.rb', line 811 def focus_reporting? @focus_reporting end |
#hide_cursor ⇒ Object
1127 1128 1129 |
# File 'lib/echoes/screen.rb', line 1127 def hide_cursor @cursor.visible = false end |
#insert_chars(n = 1) ⇒ Object
576 577 578 579 580 581 582 583 584 |
# File 'lib/echoes/screen.rb', line 576 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
540 541 542 543 544 545 546 547 548 549 550 551 |
# File 'lib/echoes/screen.rb', line 540 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.
959 960 961 |
# File 'lib/echoes/screen.rb', line 959 def last_completed_command_mark @command_marks.reverse_each.find { |m| m[:output_end] } end |
#line_feed ⇒ Object
456 457 458 459 460 461 462 463 |
# File 'lib/echoes/screen.rb', line 456 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
842 843 844 |
# File 'lib/echoes/screen.rb', line 842 def mark_all_dirty @dirty_rows = Set.new((0...@rows).to_a) end |
#mark_dirty(row) ⇒ Object
838 839 840 |
# File 'lib/echoes/screen.rb', line 838 def mark_dirty(row) @dirty_rows << row end |
#move_cursor(row, col) ⇒ Object
405 406 407 408 409 410 411 412 413 |
# File 'lib/echoes/screen.rb', line 405 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
446 447 448 449 |
# File 'lib/echoes/screen.rb', line 446 def move_cursor_backward(n = 1) @pending_wrap = false @cursor.col = [0, @cursor.col - n].max end |
#move_cursor_down(n = 1) ⇒ Object
421 422 423 424 425 |
# File 'lib/echoes/screen.rb', line 421 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
441 442 443 444 |
# File 'lib/echoes/screen.rb', line 441 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
427 428 429 430 431 432 |
# File 'lib/echoes/screen.rb', line 427 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
434 435 436 437 438 439 |
# File 'lib/echoes/screen.rb', line 434 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
415 416 417 418 419 |
# File 'lib/echoes/screen.rb', line 415 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
1042 1043 1044 1045 1046 1047 1048 1049 |
# File 'lib/echoes/screen.rb', line 1042 def origin_mode=(val) @origin_mode = val @pending_wrap = false if val @cursor.row = @scroll_top @cursor.col = 0 end end |
#origin_mode? ⇒ Boolean
1038 1039 1040 |
# File 'lib/echoes/screen.rb', line 1038 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.
911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 |
# File 'lib/echoes/screen.rb', line 911 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.
989 990 991 992 993 994 995 996 |
# File 'lib/echoes/screen.rb', line 989 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
834 835 836 |
# File 'lib/echoes/screen.rb', line 834 def pop_title @title = @title_stack.pop if @title_stack.any? end |
#push_title ⇒ Object
830 831 832 |
# File 'lib/echoes/screen.rb', line 830 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, z_index: 0) ⇒ 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 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 |
# 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, z_index: 0) 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 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. Oversize # placements are registered at the cursor anyway — only # the cells inside the grid get reserved as multicell # anchors / continuations; the rest of the image draws # via the GUI's blit pass and clips at the pane rect. # (Used to bail out entirely on oversize; presentation # clients prefer "show what fits" to "show nothing.") else return if mc_cols > @cols || mc_rows > @rows 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 # How many cells of the multicell footprint actually land # inside the grid. For images that fit, this equals the full # mc_cols / mc_rows; oversize C=1 placements get clamped so # we never index past `@grid`. The placement entry below # still records the full mc_cols × mc_rows so the GUI draws # the unclipped image and the pane-rect clip handles the # overflowing pixels. reservable_cols = [mc_cols, @cols - anchor_col].min reservable_rows = [mc_rows, @rows - anchor_row].min return if reservable_cols < 1 || reservable_rows < 1 reservable_rows.times do |dr| reservable_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. `cols` / `rows` carry the *full* image extent # — the GUI uses those for the pixel draw rect — even when # only `reservable_*` of those cells actually fit on the # grid. anchor.multicell = { cols: mc_cols, rows: mc_rows, scale: 1, frac_n: 0, frac_d: 0, valign: 0, halign: 0, } reservable_rows.times do |dr| reservable_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, z_index: z_index.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
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 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 |
# File 'lib/echoes/screen.rb', line 349 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.
864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 |
# File 'lib/echoes/screen.rb', line 864 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
1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 |
# File 'lib/echoes/screen.rb', line 1197 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
1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 |
# File 'lib/echoes/screen.rb', line 1213 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
781 782 783 784 785 786 787 788 789 790 791 792 793 |
# File 'lib/echoes/screen.rb', line 781 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
465 466 467 468 469 470 471 472 |
# File 'lib/echoes/screen.rb', line 465 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
766 767 768 769 770 771 772 773 774 775 776 777 778 779 |
# File 'lib/echoes/screen.rb', line 766 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
627 628 629 630 631 632 633 634 635 636 |
# File 'lib/echoes/screen.rb', line 627 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
595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 |
# File 'lib/echoes/screen.rb', line 595 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
1135 1136 1137 1138 1139 1140 1141 1142 1143 |
# File 'lib/echoes/screen.rb', line 1135 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
1021 1022 1023 |
# File 'lib/echoes/screen.rb', line 1021 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.
967 968 969 970 |
# File 'lib/echoes/screen.rb', line 967 def set_current_command_text(text) return unless @current_command_mark @current_command_mark[:command_text] = text end |
#set_graphics(params) ⇒ Object
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 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 |
# File 'lib/echoes/screen.rb', line 646 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
850 851 852 |
# File 'lib/echoes/screen.rb', line 850 def set_hyperlink(uri) @attrs.hyperlink = uri end |
#set_scroll_region(top, bottom) ⇒ Object
638 639 640 641 642 643 644 |
# File 'lib/echoes/screen.rb', line 638 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
488 489 490 491 |
# File 'lib/echoes/screen.rb', line 488 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.
621 622 623 624 625 |
# File 'lib/echoes/screen.rb', line 621 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
1123 1124 1125 |
# File 'lib/echoes/screen.rb', line 1123 def show_cursor @cursor.visible = true end |
#soft_reset ⇒ Object
1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 |
# File 'lib/echoes/screen.rb', line 1160 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
1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 |
# File 'lib/echoes/screen.rb', line 1055 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
1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 |
# File 'lib/echoes/screen.rb', line 1083 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
474 475 476 477 478 |
# File 'lib/echoes/screen.rb', line 474 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).
940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 |
# File 'lib/echoes/screen.rb', line 940 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
1131 1132 1133 |
# File 'lib/echoes/screen.rb', line 1131 def to_text @grid.map { |row| row.map { |cell| cell.char }.join.rstrip }.join("\n").rstrip end |
#using_alt_screen? ⇒ Boolean
1051 1052 1053 |
# File 'lib/echoes/screen.rb', line 1051 def using_alt_screen? @using_alt_screen end |
#word_boundaries_at(row, col) ⇒ Object
1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 |
# File 'lib/echoes/screen.rb', line 1145 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 |