Class: RubyTerm
- Inherits:
-
Object
- Object
- RubyTerm
- Defined in:
- lib/rubyterm/app.rb,
lib/rubyterm/version.rb
Overview
The X11 terminal application: owns the X window, the pty controller, the input-processing thread and the blink/flush timers, and wires the interpreter (Term) + damage tracker (TrackChanges) + buffer to the X11 backend (Window via WindowAdapter). The reusable terminal engine lives in the other lib/ files; this class is the executable front end (bin/rubyterm).
Component classes and the X11/skrift/toml dependencies are loaded by lib/rubyterm.rb, which requires this file last.
Constant Summary collapse
- TOPMOST =
0- LEFTMOST =
0- JUMP_BACKLOG =
Everything that touches the buffer/window runs here, on the single input-processing thread. The blink and flush timers enqueue :blink and :flush rather than touching the buffer from their own threads: the buffer/renderer is not thread-safe, and concurrent mutation (e.g. blink's redraw racing a feed mid-scroll) corrupts cells non-deterministically. Serializing through the queue is the synchronization. When more than this many items are already queued behind the chunk we just processed, we treat it as a flood and jump-scroll: keep interpreting (the buffer + scrollback still update) but stop rendering every frame. The screen catches up at the flush tick or when we drain.
8- WHEEL_SCROLL_LINES =
Lines the view moves per mouse wheel tick (Shift+PageUp/Down move a full page of 10).
3- VERSION =
"0.2.8"
Instance Attribute Summary collapse
-
#blink_state ⇒ Object
readonly
Returns the value of attribute blink_state.
-
#rblink_state ⇒ Object
readonly
Returns the value of attribute rblink_state.
Instance Method Summary collapse
- #adjust_fontsize(delta) ⇒ Object
- #bg ⇒ Object
- #blink ⇒ Object
- #char_h ⇒ Object
- #char_w ⇒ Object
- #clear_selection_if_set ⇒ Object
-
#coalesced_redraw ⇒ Object
Process a coalesced redraw on the processing thread: resize to the latest pending size if it changed, otherwise just repaint.
- #each_character(&block) ⇒ Object
- #event_thread ⇒ Object
-
#exit_scrollback ⇒ Object
Sending input to the pty must snap the view back to the live screen; otherwise typed/echoed output is drawn over the scrolled-back display.
- #fg ⇒ Object
- #get_selection ⇒ Object
- #get_x ⇒ Object
- #get_y ⇒ Object
- #handle_mouse(pkt) ⇒ Object
- #initconfig ⇒ Object
-
#initialize(args) ⇒ RubyTerm
constructor
A new instance of RubyTerm.
- #inspect ⇒ Object
-
#jump_redraw ⇒ Object
Resume rendering and repaint the whole current screen at once - the jump-scroll "catch up", skipping every frame that scrolled past while suspended.
- #key(event) ⇒ Object
- #process(pkt) ⇒ Object
- #process_chunk(str) ⇒ Object
- #process_queue ⇒ Object
-
#reapply_selection ⇒ Object
Re-stamp the active selection highlight on top of freshly drawn content.
- #redraw ⇒ Object
- #redraw_positions(positions) ⇒ Object
-
#render_selection ⇒ Object
FIXME: Cursor, selection etc.
- #render_text_buffer ⇒ Object
-
#repaint_view_row(sy) ⇒ Object
Repaint one on-screen row from the buffer at the current view offset (blank cells paint as spaces, so this also erases stale pixels).
-
#request_redraw(size = nil) ⇒ Object
Request a redraw (from the event thread).
- #resize(w, h) ⇒ Object
- #run(args) ⇒ Object
- #scrollback_view_down(lines = 10) ⇒ Object
-
#scrollback_view_up(lines = 10) ⇒ Object
Move the scrollback view up/down by
linesand repaint incrementally: blit the rows that stay on screen (the same CopyArea mechanism live scrolling uses) and repaint only the newly exposed ones. -
#selection_bounds ⇒ Object
The selection's [start, end] boundary pairs, expanded so a double-width glyph is always whole: if the first selected cell is a WIDE_SPACER tail, include its head; if the last selected cell is a wide head, include its tail.
-
#set_columns(cols) ⇒ Object
DECCOLM: realise an 80/132 column switch (called via the adapter from Term#set_width_and_clear).
-
#shift_selection_for_scroll(start, bottom) ⇒ Object
A line of the scroll region [start..bottom] has scrolled up into history.
- #term_height ⇒ Object
- #term_width ⇒ Object
-
#write(str) ⇒ Object
Escape/control/character interpretation lives in Term (lib/term.rb).
Constructor Details
#initialize(args) ⇒ RubyTerm
Returns a new instance of RubyTerm.
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 79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/rubyterm/app.rb', line 33 def initialize(args) initconfig @queue = Queue.new # Coalesce redraw-causing events (resize/expose): a drag fires a # flood of ConfigureNotify + Expose, and repainting on each one makes # the display lag while it drains the backlog. Keep at most one # redraw request queued, always for the latest pending size. @redraw_mutex = Mutex.new @redraw_pending = false @pending_resize = nil # DECCOLM (80/132 column switch) mode: :font rescales the glyph cell to # fit the new column count in the current window (reliable everywhere); # :window asks the WM to resize the window. Default :font. @deccolm_mode = (@config[:deccolm] || "font").to_s.to_sym @window = Window.new(fonts: @config[:fonts], fontsize: @config[:fontsize], width: @config[:width], height: @config[:height]) @adapter = WindowAdapter.new(@window, self) # Yes, this is "bad" and we should define our # own, however, I'd prefer to match rxvt or similar # sufficiently that we can rely on a TERM setting that # "everyone" already has in their termcap. rxvt seems to # work better than xterm, but will adjust and consider # providing multiple modes ENV["TERM"] = "rxvt-256color" ENV["COLORTERM"] = "truecolor" while args[0].to_s[0] == ?- case args[0] when "--" args.shift break when "-c" args.shift @term_instance = args.shift else break end end @window.dpy.change_property(:replace, @window.wid, "WM_CLASS", @window.dpy.atom("STRING"), 8, "rterm\0#{@term_instance||'rterm'}\0".unpack("C*")) @window.map_window @buffer = TrackChanges.new(TermBuffer.new, @adapter) @buffer.defer = true # damage-driven rendering: set() mutates, flush draws @term = Term.new(@buffer) @buffer.on_resize(@term.width, @term.height) # Give window access to the buffer for scrollback @window.set_buffer(@buffer) end |
Instance Attribute Details
#blink_state ⇒ Object (readonly)
Returns the value of attribute blink_state.
13 14 15 |
# File 'lib/rubyterm/app.rb', line 13 def blink_state @blink_state end |
#rblink_state ⇒ Object (readonly)
Returns the value of attribute rblink_state.
13 14 15 |
# File 'lib/rubyterm/app.rb', line 13 def rblink_state @rblink_state end |
Instance Method Details
#adjust_fontsize(delta) ⇒ Object
274 275 276 277 278 279 |
# File 'lib/rubyterm/app.rb', line 274 def adjust_fontsize(delta) @window.adjust_fontsize(delta) resize(@pixelw,@pixelh) @window.clear(0,0,@pixelw,@pixelh) redraw end |
#bg ⇒ Object
172 |
# File 'lib/rubyterm/app.rb', line 172 def bg = @term.bg |
#blink ⇒ Object
339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 |
# File 'lib/rubyterm/app.rb', line 339 def blink t = Time.now doblink = false #p [@blink_state, @rblink_state, @lastblink, @lastrblink] if ((t - @lastblink)*10).to_i > 6 @lastblink = t @blink_state = !@blink_state doblink = true end if ((t - @lastrblink)*10).to_i >= 2 @lastrblink = t @rblink_state = !@rblink_state doblink = true end # FIXME: It bugs out at some point? @buffer.redraw_blink if doblink end |
#char_h ⇒ Object
16 |
# File 'lib/rubyterm/app.rb', line 16 def char_h = @adapter.char_h |
#char_w ⇒ Object
15 |
# File 'lib/rubyterm/app.rb', line 15 def char_w = @adapter.char_w |
#clear_selection_if_set ⇒ Object
538 539 540 541 542 543 544 545 |
# File 'lib/rubyterm/app.rb', line 538 def clear_selection_if_set return if !@select_startpos sb = @window.scrollback_count (@selection_damage || []).each { |x,sy| @buffer.redraw_display(x, sy, sb) } @select_startpos = nil # FIXME redraw end |
#coalesced_redraw ⇒ Object
Process a coalesced redraw on the processing thread: resize to the latest pending size if it changed, otherwise just repaint.
261 262 263 264 265 266 267 268 269 270 271 272 |
# File 'lib/rubyterm/app.rb', line 261 def coalesced_redraw size = @redraw_mutex.synchronize do @redraw_pending = false @pending_resize end if size && size != @last_resize @last_resize = size resize(size[0], size[1]) # resize repaints as needed else redraw end end |
#each_character(&block) ⇒ Object
116 |
# File 'lib/rubyterm/app.rb', line 116 def each_character(&block) = @buffer.each_character(&block) |
#event_thread ⇒ Object
645 646 647 648 649 650 651 652 653 654 655 656 657 658 |
# File 'lib/rubyterm/app.rb', line 645 def event_thread Thread.new do loop do pkt = @window.dpy.next_packet begin process(pkt) rescue => e # One bad event must not kill the terminal (abort_on_exception is on). warn("rubyterm: #{e.class}: #{e.}") rescue nil end Thread.pass end end end |
#exit_scrollback ⇒ Object
Sending input to the pty must snap the view back to the live screen; otherwise typed/echoed output is drawn over the scrolled-back display.
359 360 361 |
# File 'lib/rubyterm/app.rb', line 359 def exit_scrollback redraw if @window.scrollback_reset end |
#fg ⇒ Object
171 |
# File 'lib/rubyterm/app.rb', line 171 def fg = @term.fg |
#get_selection ⇒ Object
524 525 526 527 528 529 530 531 532 533 534 535 536 |
# File 'lib/rubyterm/app.rb', line 524 def get_selection startpos, endpos = selection_bounds str = "" ypos = nil @buffer.each_character_between(startpos[0]..startpos[1], endpos[0]..endpos[1]) do |x,y,cell| str += "\n" if ypos && y != ypos ypos = y cp = cell && cell[0] next if cp == CharWidth::WIDE_SPACER # tail of a double-width glyph: its head already emitted the char str << (cp.chr(Encoding::UTF_8) rescue "") end str end |
#get_x ⇒ Object
30 |
# File 'lib/rubyterm/app.rb', line 30 def get_x = @term.x |
#get_y ⇒ Object
31 |
# File 'lib/rubyterm/app.rb', line 31 def get_y = @term.y |
#handle_mouse(pkt) ⇒ Object
547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 |
# File 'lib/rubyterm/app.rb', line 547 def handle_mouse(pkt) @term. = = pkt.detail > 0 ? pkt.detail : @term. release = pkt.is_a?(X11::Form::ButtonRelease) x = pkt.event_x / char_w y = pkt.event_y / char_h # Holding Shift forces a local text selection even when the # application has grabbed the mouse (mouse reporting on - e.g. Claude's # agent picker, or any full-screen app with clickable UI). This is the # standard xterm override so you can always select/copy. shift = pkt.state.anybits?(0x01) # ShiftMask case shift ? nil : @term.mouse_mode when nil # Wheel ticks (buttons 4/5) scroll the local scrollback view, the way # every other terminal scrolls natively - this is what an app that # does not grab the mouse (e.g. a shell, or claude's inline mode) # relies on. Handled before the selection logic below so a wheel tick # never starts or clears a selection. With Shift held this branch is # forced even when an app owns the mouse, so Shift+wheel always # scrolls history - the same override as Shift+select. if >= 4 return if release # each tick is a press+release pair; act on press scrollback_view_up(WHEEL_SCROLL_LINES) if == 4 scrollback_view_down(WHEEL_SCROLL_LINES) if == 5 return end # Selection coordinates are cell *boundaries* (round to the nearest # column edge, not the floor cell), so dragging across a single cell # selects exactly that cell and a click (no boundary crossed) selects # none. Mouse reporting below keeps the floor cell index. x = (pkt.event_x + char_w / 2) / char_w # Selection works in buffer coordinates: when scrolled back, the # row under the pointer is a scrollback line (buffer row # screen_y - scrollback_count, negative for scrollback). Without # this, selection/copy reads the live screen instead of what is # actually displayed. y -= @window.scrollback_count # New selection, but the old has not been cleared yet if @released clear_selection_if_set @released = false end @select_startpos ||= [x,y] if [x,y] != @select_endpos @select_endpos = [x,y] # FIXME: Optimize rendering of selection further render_selection end if release @released = true if @select_startpos != @select_endpos sel = get_selection io = IO.popen("xsel -i", "a+") io.write(sel) io.close else clear_selection_if_set end end when :vt200, :btn_event, :any_event # FIXME: This is only right for @mouse_reporting == :digits # FIXME: Report modifiers. # FIXME: Motion reporting is the same for all three modes; strictly # :vt200 (1000) reports no motion and :any_event (1003) reports all # motion, not just with a button held. # Not reporting release for scroll wheel return if release && >= 4 event = [0,1,2,64,65][-1] event += 32 if pkt.is_a?(X11::Form::MotionNotify) #button = [0,1,2,4,5][button-1] @controller.mouse_report(@term.mouse_reporting, event, x,y, release) end end |
#initconfig ⇒ Object
20 21 22 23 24 25 26 |
# File 'lib/rubyterm/app.rb', line 20 def initconfig cname = File.("~/.config/rterm/config.toml") if File.exist?(cname) @config = TomlRB.load_file(cname, symbolize_keys: true) end @config ||= {} end |
#inspect ⇒ Object
28 |
# File 'lib/rubyterm/app.rb', line 28 def inspect = "<RubyTerm #{self.object_id}>" |
#jump_redraw ⇒ Object
Resume rendering and repaint the whole current screen at once - the jump-scroll "catch up", skipping every frame that scrolled past while suspended.
239 240 241 242 |
# File 'lib/rubyterm/app.rb', line 239 def jump_redraw @buffer.suspend = false redraw end |
#key(event) ⇒ Object
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 |
# File 'lib/rubyterm/app.rb', line 281 def key(event) #p event ks, str = lookup_string(@window.dpy, event) case ks when :"ctrl_+" then adjust_fontsize(1.0) when :"ctrl_-" then adjust_fontsize(-1.0) when :shift_page_up scrollback_view_up return when :shift_page_down scrollback_view_down return when :XK_Insert # Paste primary selection # FIXME: Giant hack primary = `xsel -p` if primary.chomp.empty? primary = @primary else @primary = primary end exit_scrollback @controller.paste(primary) return when "C" # FIXME. Cstrl + shift + c if str == "\x03" # Copy primary selection into clipboard system("xsel -o -p | xsel -i -b") return end when "V" # FIXME. Cstrl + shift + v if str == "\x16" # Paste clipboard clipboard = `xsel -b` if clipboard.chomp.empty? clipboard = @clipboard else @clipboard = clipboard end exit_scrollback @controller.paste(clipboard) return end when :XK_Menu; # FIXME: Want deskmenu here, but as long as we're not running the shell, we don't know pwd. puts "FIXME: deskmenu" # FIXME: In the meantime we use it as a debugging tool to force redraw. redraw render_text_buffer end payload = keysym_to_vt102(ks) || str # Only snap to the live screen when we're actually sending input; # bare modifiers (Ctrl/Shift) produce no payload and must not disturb # scrollback (e.g. while setting up a Ctrl+Shift+C copy from history). exit_scrollback unless payload.nil? || payload.empty? @controller.keypress(payload) end |
#process(pkt) ⇒ Object
622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 |
# File 'lib/rubyterm/app.rb', line 622 def process(pkt) # p pkt case pkt when X11::Form::ButtonPress, X11::Form::MotionNotify, X11::Form::ButtonRelease handle_mouse(pkt) when X11::Form::KeyPress key(pkt) when X11::Form::KeyRelease, X11::Form::NoExposure # Intentionally ignored when X11::Form::ConfigureNotify # Real size change: pkt.width/height are the new window size. request_redraw([pkt.width, pkt.height]) when X11::Form::Expose # Damage, NOT a size change: pkt.width/height are the exposed # rectangle, not the window. Repaint only; never resize here (that # would shrink the terminal to the strip size). request_redraw else # Other X events (MapNotify, etc.) are not acted on. end end |
#process_chunk(str) ⇒ Object
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 |
# File 'lib/rubyterm/app.rb', line 198 def process_chunk(str) case str when :blink then return blink when :flush # Mid-flood: jump the display to the current state at the flush rate # instead of painting every scrolled-off frame. jump_redraw if @buffer.suspend return @window.flush when :do_redraw then return coalesced_redraw end # FIXME: Could be smarter about this; it's only needed if the # first character being written won't clear the same square. @term.clear_cursor @term.feed(str) if @queue.size > JUMP_BACKLOG # A flood is backing up. Render this frame, then suspend per-chunk # rendering: subsequent chunks only mutate the buffer until the flush # tick jumps the display forward or we catch up. @buffer.draw_flush @buffer.suspend = true elsif @buffer.suspend jump_redraw # drained after a flood: redraw the final state else # Draw the chunk's damaged content (damage-driven flush), then the # cursor overlay on top. @buffer.draw_flush @term.draw_cursor @buffer.draw_flush # Ensure everything has been rendered # Output just repainted cells without the selection overlay; re-stamp # it so a streaming program (top, full-screen apps) doesn't erase the # highlight out from under an in-progress copy. reapply_selection end end |
#process_queue ⇒ Object
183 |
# File 'lib/rubyterm/app.rb', line 183 def process_queue = process_chunk(@queue.shift) |
#reapply_selection ⇒ Object
Re-stamp the active selection highlight on top of freshly drawn content. The selection is an overlay that is NOT stored in the buffer, so any output - or a full redraw - that repaints those cells erases the highlight. Re-applying it after each draw keeps the selection visible while a program streams output (e.g. top repainting, or a full-screen app), which a one-shot paint at mouse-time cannot do.
466 467 468 469 470 471 472 473 474 475 476 477 |
# File 'lib/rubyterm/app.rb', line 466 def reapply_selection return unless @select_startpos && @select_endpos return if @select_startpos == @select_endpos # zero-width (click): nothing selected sb = @window.scrollback_count spos, epos = selection_bounds @buffer.each_character_between(spos[0]..spos[1], epos[0]..epos[1]) do |x,y,cell| sy = y + sb next if sy < 0 || sy >= @term.height @buffer.redraw_cell_at(x, sy, cell, fg: 0xffffff, bg: 0xff00ff) end @buffer.draw_flush end |
#redraw ⇒ Object
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 |
# File 'lib/rubyterm/app.rb', line 91 def redraw # Always clear the entire screen before redrawing to ensure all areas are cleaned up # This ensures no artifacts remain at the ends of lines @window.clear(0, 0, @window.width, @window.height) if @window.scrollback_mode @buffer.redraw_all(@window.scrollback_count) else @buffer.redraw_all(0) end # Make sure changes are rendered and cursor is shown @buffer.draw_flush @term.draw_cursor # A full repaint draws only buffer content; restore the selection # overlay on top so it survives resizes, exposes and scrollback redraws. reapply_selection end |
#redraw_positions(positions) ⇒ Object
439 |
# File 'lib/rubyterm/app.rb', line 439 def redraw_positions(positions) = positions.each { |pos| @buffer.redraw(*pos) } |
#render_selection ⇒ Object
FIXME: Cursor, selection etc. are "special" overlays on top of attributes. Allow the terminal to set a set of positions + fg/bg, and a set of ranges.
500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 |
# File 'lib/rubyterm/app.rb', line 500 def render_selection # Selection positions are in *buffer* coordinates (negative rows are # scrollback); the screen row is buffer_row + scrollback_count. Damage # is tracked in screen coordinates so it can be repainted later. sb = @window.scrollback_count olddamage = @selection_damage || Set.new @selection_damage = Set.new # A zero-width (click) selection highlights nothing, but still falls # through to clear any previous highlight below. unless @select_startpos == @select_endpos spos, epos = selection_bounds @buffer.each_character_between(spos[0]..spos[1], epos[0]..epos[1]) do |x,y,cell| sy = y + sb next if sy < 0 || sy >= @term.height @selection_damage << [x,sy] @buffer.redraw_cell_at(x, sy, cell, fg: 0xffffff, bg: 0xff00ff) end end # Repaint cells that left the selection with their displayed content. (olddamage - @selection_damage).each { |x,sy| @buffer.redraw_display(x, sy, sb) } @buffer.draw_flush #@window.flush end |
#render_text_buffer ⇒ Object
110 111 112 113 114 |
# File 'lib/rubyterm/app.rb', line 110 def render_text_buffer (0...@term.height).each {|y| puts @buffer.buffer.getline(y).map {|a| a ? a[0].chr(Encoding::UTF_8):" " }.join } end |
#repaint_view_row(sy) ⇒ Object
Repaint one on-screen row from the buffer at the current view offset (blank cells paint as spaces, so this also erases stale pixels).
434 435 436 437 |
# File 'lib/rubyterm/app.rb', line 434 def repaint_view_row(sy) sb = @window.scrollback_count (0...@term.width).each { |x| @buffer.redraw_display(x, sy, sb) } end |
#request_redraw(size = nil) ⇒ Object
Request a redraw (from the event thread). Records the latest pending size and enqueues a single :do_redraw marker; while one is already queued, further requests just update the target. This collapses a drag's flood of ConfigureNotify+Expose into one repaint per processing slot at the most recent size, instead of one per event.
249 250 251 252 253 254 255 256 257 |
# File 'lib/rubyterm/app.rb', line 249 def request_redraw(size = nil) enqueue = false @redraw_mutex.synchronize do @pending_resize = size if size enqueue = true unless @redraw_pending @redraw_pending = true end @queue << :do_redraw if enqueue end |
#resize(w, h) ⇒ Object
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
# File 'lib/rubyterm/app.rb', line 118 def resize(w,h) @pixelw||=0 @pixelh||=0 should_redraw = w >= @pixelw || h >= @pixelh @pixelw=w @pixelh=h @window.on_resize(w,h) w = w/char_w h = h/char_h return if w <= 0 || h <= 0 # FIXME: WTF?!? #if w != @term.width && h == @term.height @buffer.on_resize(w,h) ow, oh = @term.width, @term.height if ow != w || oh != h @term.resize(w,h) @controller.report_size(w,h) end if should_redraw redraw end end |
#run(args) ⇒ Object
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 |
# File 'lib/rubyterm/app.rb', line 661 def run(args) @controller = Controller.new(self, @config) @controller.run(*args) @term.responder = @controller @lastblink ||= Time.now @lastrblink ||= Time.now Thread.abort_on_exception = true threads =[] threads << Thread.new do loop do process_queue Thread.pass end end threads << event_thread threads << Thread.new do loop do sleep(0.1) # Enqueue rather than calling blink directly: blink redraws and # so touches the buffer, which must only be mutated on the # processing thread. blink self-gates on elapsed time, so a # fixed tick is fine. @queue << :blink end end # Flush on the processing thread too, so the @buf->window copy never # races a concurrent buffer mutation. threads << Thread.new do loop do @queue << :flush sleep(1/30.0) end end if ENV["DEBUG"].to_s.strip != "" while cmd = STDIN.gets&.strip binding.pry if cmd == "pry" end end threads.each(&:join) end |
#scrollback_view_down(lines = 10) ⇒ Object
404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 |
# File 'lib/rubyterm/app.rb', line 404 def scrollback_view_down(lines = 10) # Don't do anything if not in scrollback mode return if !@window.scrollback_mode moved = @window.scrollback_scroll(-lines) return if moved == 0 exited = !@window.scrollback_mode # landed back on the live screen rows = @term.height if moved >= rows redraw else # Shift the surviving rows up (clears the vacated bottom band; the # indicator is re-drawn only while still scrolled back). @window.scroll_up(moved * char_h, @window.width, (rows - moved) * char_h, moved * char_h) # Repaint the rows that scrolled into view at the bottom. (rows - moved...rows).each { |sy| repaint_view_row(sy) } @buffer.draw_flush @window.flush reapply_selection end # Explicitly force cursor to be redrawn if exiting scrollback mode if exited @term.clear_cursor @term.draw_cursor @buffer.draw_flush @window.flush end end |
#scrollback_view_up(lines = 10) ⇒ Object
Move the scrollback view up/down by lines and repaint incrementally:
blit the rows that stay on screen (the same CopyArea mechanism live
scrolling uses) and repaint only the newly exposed ones. The previous
full clear+redraw per step made wheel scrolling visibly jerky. Shared
by Shift+PageUp/Down and the wheel.
The blit carries the selection-highlight pixels along with their text (the selection is pinned to buffer content), so only the freshly repainted rows need the overlay re-stamped - reapply_selection does that at the end.
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 |
# File 'lib/rubyterm/app.rb', line 377 def scrollback_view_up(lines = 10) # Nothing above the view: don't touch the cursor or repaint at all # (wheel ticks arrive far more often than Shift+PageUp ever did). return if @window.scrollback_count >= @window.scrollback_buffer_size if !@window.scrollback_mode # Leaving the live screen: repaint the cursor cell first - cursor # redraws are suppressed once scrolled back, and the blit below would # otherwise drag the cursor overlay along as if it were content. @term.clear_cursor @buffer.draw_flush end moved = @window.scrollback_scroll(lines) rows = @term.height # A jump of a screenful or more retains nothing worth blitting. return redraw if moved >= rows # Shift the surviving rows down by +moved+ (this also clears the # vacated top band and re-draws the scrollback indicator over row 0). @window.scroll_down(0, @window.width, (rows - moved) * char_h, moved * char_h) # Repaint the newly exposed history rows. Row 0 stays the indicator # line, so start at 1; include row +moved+, where the old top row's # indicator-overlay pixels landed. (1..moved).each { |sy| repaint_view_row(sy) } @buffer.draw_flush @window.flush reapply_selection end |
#selection_bounds ⇒ Object
The selection's [start, end] boundary pairs, expanded so a double-width glyph is always whole: if the first selected cell is a WIDE_SPACER tail, include its head; if the last selected cell is a wide head, include its tail. So clipping a selection to either half of an emoji still selects the whole emoji. Returns the raw pair unchanged when there is no selection.
446 447 448 449 450 451 452 453 454 455 456 457 458 |
# File 'lib/rubyterm/app.rb', line 446 def selection_bounds s, e = @select_startpos, @select_endpos return [s, e] unless s && e lo, hi = [s, e].sort_by { |x, y| [y, x] } # Boundaries -> inclusive cell range: a selection from boundary lo to hi # covers cells [lo .. hi-1]. hi = [hi[0] - 1, hi[1]] c = @buffer.get(lo[0], lo[1]) lo = [lo[0] - 1, lo[1]] if lo[0] > 0 && c && c[0] == CharWidth::WIDE_SPACER last = @buffer.get(hi[0], hi[1]) hi = [hi[0] + 1, hi[1]] if last && last[0] && CharWidth.width(last[0]) == 2 [lo, hi] end |
#set_columns(cols) ⇒ Object
DECCOLM: realise an 80/132 column switch (called via the adapter from
Term#set_width_and_clear). font mode rescales the glyph cell so cols
columns fit the current window, keeping the row count; window mode asks
the WM to resize. Either way the pty is told the new size.
147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 |
# File 'lib/rubyterm/app.rb', line 147 def set_columns(cols) return if @deccolm_mode == :off # ignore DECCOLM entirely cols = cols.to_i return if cols <= 0 || @pixelw.to_i <= 0 if @deccolm_mode == :window # WM-driven: the resulting ConfigureNotify completes the change via resize(). @window.request_pixel_size(cols * char_w, @pixelh.to_i) return end # font mode. Rescale the glyph cell (up or down) so `cols` columns fit # the current window, keeping the row count. The window is NOT resized; # integer cell widths mean the rightmost columns may not reach the window # edge exactly (an accepted artefact - we don't do sub-pixel placement). rows = @term.height @window.fit_columns(cols, @pixelw.to_i) @buffer.on_resize(cols, rows) @term.resize(cols, rows) @controller.report_size(cols, rows) redraw end |
#shift_selection_for_scroll(start, bottom) ⇒ Object
A line of the scroll region [start..bottom] has scrolled up into history. The selection is stored in buffer coordinates (negative rows
scrollback), so the content the user selected now sits one row higher:
cells inside the scrolled region - and anything already in scrollback, whose negative index shifts as the history grows - move up by one, while content below the region stays put. Shift the stored selection to match so reapply_selection keeps the highlight pinned to the same text as it scrolls (into scrollback and back), instead of leaving it on whatever rolls into the old screen rows. Called from TrackChanges#scroll_up via the adapter for every line moved into history.
489 490 491 492 493 494 495 496 |
# File 'lib/rubyterm/app.rb', line 489 def shift_selection_for_scroll(start, bottom) return unless @select_startpos [@select_startpos, @select_endpos].each do |pos| next unless pos y = pos[1] pos[1] -= 1 if y < 0 || (y >= start && y <= bottom) end end |
#term_height ⇒ Object
18 |
# File 'lib/rubyterm/app.rb', line 18 def term_height = @term.height |
#term_width ⇒ Object
17 |
# File 'lib/rubyterm/app.rb', line 17 def term_width = @term.width |
#write(str) ⇒ Object
Escape/control/character interpretation lives in Term (lib/term.rb). RubyTerm only owns the X11 window, the pty controller and the threading; this keeps the terminal core testable headlessly (see harness/).
179 180 181 |
# File 'lib/rubyterm/app.rb', line 179 def write(str) @queue << str end |