Class: Everywhere::Dock::Screen
- Inherits:
-
Object
- Object
- Everywhere::Dock::Screen
- Defined in:
- lib/everywhere/dock/screen.rb
Overview
Emits the escape sequences that keep the footer pinned to the bottom.
THE MECHANIC, and why it isn't the obvious one:
The tempting approach is DECSTBM — set a scroll region (ESC[1;Nr) that excludes the last few rows and let the terminal itself keep output off them. It works, and it even confines child processes writing straight to the fd. It was rejected for two reasons:
1. It destroys scrollback. tmux only copies scrolled-out rows into its
history when the scroll region is the full screen
(grid_view_scroll_region_up checks rupper == 0 && rlower == sy - 1);
screen and, as far as we can tell, iTerm2 behave the same way. Losing
the ability to scroll up and read a Rails backtrace is a far worse
regression than the one the dock is fixing.
2. Its failure mode is terrible. If the CLI is SIGKILLed before it can
emit ESC[r, the user's terminal keeps a dead band at the bottom until
they run `reset`.
So instead we own every byte (the dev server and shells are relayed through us, not inherited) and repaint: erase the footer rows, write the new content — which scrolls normally, preserving scrollback everywhere — then draw the footer again. This is what indicatif, Ink, docker compose and cargo all do. Worst-case failure is one stale line on screen.
Everything is emitted as a SINGLE write per frame. Byte interleaving between processes happens between write(2) calls, not inside one.
Constant Summary collapse
- ERASE_ROW =
clear this row, cursor back to column 1
"\r\e[2K"- UP_ROW =
up one row and clear it
"\e[1A\e[2K"- HIDE =
"\e[?25l"- SHOW =
"\e[?25h"- DEFAULT_SIZE =
[24, 80].freeze
Instance Method Summary collapse
-
#clear ⇒ Object
Wipe the footer without redrawing it — for anything that has to write outside our frame discipline (stderr, an interactive child).
- #close ⇒ Object
- #cols ⇒ Object
-
#frame(content, footer) ⇒ Object
content: text to land above the footer (must end in a newline, or be empty).
-
#initialize(io:, size: nil) ⇒ Screen
constructor
A new instance of Screen.
- #open ⇒ Object
- #rows ⇒ Object
-
#size ⇒ Object
Rows and columns, re-read on every resize.
Constructor Details
#initialize(io:, size: nil) ⇒ Screen
Returns a new instance of Screen.
40 41 42 43 44 45 |
# File 'lib/everywhere/dock/screen.rb', line 40 def initialize(io:, size: nil) @io = io @size = size @drawn = 0 @open = false end |
Instance Method Details
#clear ⇒ Object
Wipe the footer without redrawing it — for anything that has to write outside our frame discipline (stderr, an interactive child).
71 72 73 74 75 76 |
# File 'lib/everywhere/dock/screen.rb', line 71 def clear return unless @open && @drawn.positive? @io.write(erase) @drawn = 0 end |
#close ⇒ Object
78 79 80 81 82 83 84 85 |
# File 'lib/everywhere/dock/screen.rb', line 78 def close return unless @open @io.write("#{erase}#{SHOW}") @drawn = 0 @open = false @io.sync = @sync end |
#cols ⇒ Object
101 |
# File 'lib/everywhere/dock/screen.rb', line 101 def cols = size[1] |
#frame(content, footer) ⇒ Object
content: text to land above the footer (must end in a newline, or be empty). footer: the rows to pin.
58 59 60 61 62 63 64 65 66 67 |
# File 'lib/everywhere/dock/screen.rb', line 58 def frame(content, ) return unless @open out = +"" out << erase out << content out << .join("\n") @drawn = .length @io.write(out) end |
#open ⇒ Object
47 48 49 50 51 52 53 54 |
# File 'lib/everywhere/dock/screen.rb', line 47 def open return if @open @open = true @sync = @io.sync @io.sync = true @io.write(HIDE) end |
#rows ⇒ Object
100 |
# File 'lib/everywhere/dock/screen.rb', line 100 def rows = size[0] |
#size ⇒ Object
Rows and columns, re-read on every resize. IO#winsize is a plain
TIOCGWINSZ ioctl — it does not touch termios, so it is safe alongside
the stty -icanon -echo the key loop sets.
90 91 92 93 94 95 96 97 98 |
# File 'lib/everywhere/dock/screen.rb', line 90 def size return @size if @size require "io/console" @io.winsize rescue StandardError env = [ENV["LINES"].to_i, ENV["COLUMNS"].to_i] env.all?(&:positive?) ? env : DEFAULT_SIZE end |