Class: TTY::Command::Window::Coordinator
- Inherits:
-
Object
- Object
- TTY::Command::Window::Coordinator
- Defined in:
- lib/tty/command/window/coordinator.rb
Overview
Owns a region at the bottom of the terminal and paints the stack of active Blocks into it from a dedicated render thread.
All painted lines end with explicit "\r\n" so rendering stays correct even when the input router has put the terminal into raw mode (which disables output newline translation for the whole tty).
Constant Summary collapse
- FRAME_INTERVAL =
0.08- HIDE_CURSOR =
"\e[?25l"- SHOW_CURSOR =
"\e[?25h"
Instance Attribute Summary collapse
-
#output ⇒ Object
readonly
Returns the value of attribute output.
-
#pastel ⇒ Object
readonly
Returns the value of attribute pastel.
Class Method Summary collapse
-
.emergency_restore_all ⇒ Object
Trap-safe variant of Coordinator.restore_all: iterates a snapshot of the registry without @registry_mutex, which the main thread may hold when the signal arrives (see review P0-5).
-
.for(output, width: nil) ⇒ Coordinator
Coordinator bound to the given output IO (one per IO).
-
.restore_all ⇒ Object
Emergency cleanup for at_exit: restore the cursor everywhere.
Instance Method Summary collapse
-
#attach_session(block, session) ⇒ Object
Attach a child session to an already-registered block so signal forwarding and PTY resize reach it.
-
#color? ⇒ Boolean
Whether colored output is enabled.
-
#detach_session(block) ⇒ Object
Detach the session attached via #attach_session.
-
#emergency_restore ⇒ Object
at_exit safety net: show the cursor if we died mid-paint.
-
#finalize(block) ⇒ Object
Called by the runner when a block's command finished.
-
#initialize(output:, width_override: nil) ⇒ Coordinator
constructor
A new instance of Coordinator.
-
#mark_dirty ⇒ Object
Request a repaint on the next frame (cheap, lock-free).
-
#register(block, child_session: nil) ⇒ Object
Add a block to the stack and start rendering if needed.
-
#width ⇒ Integer
Current terminal width in columns.
-
#winch! ⇒ Object
Trap-context WINCH notification; actual work happens on the render thread.
Constructor Details
#initialize(output:, width_override: nil) ⇒ Coordinator
Returns a new instance of Coordinator.
55 56 57 58 59 60 61 62 63 64 65 66 67 |
# File 'lib/tty/command/window/coordinator.rb', line 55 def initialize(output:, width_override: nil) @output = output @width_override = width_override @blocks = [] @sessions = {} # block => ChildSession, for signal + PTY resize @painted_height = 0 @active = false @winch = false @dirty = false @mutex = Mutex.new @spinner = Spinner.new @pastel = Pastel.new(enabled: color?) end |
Instance Attribute Details
#output ⇒ Object (readonly)
Returns the value of attribute output.
53 54 55 |
# File 'lib/tty/command/window/coordinator.rb', line 53 def output @output end |
#pastel ⇒ Object (readonly)
Returns the value of attribute pastel.
53 54 55 |
# File 'lib/tty/command/window/coordinator.rb', line 53 def pastel @pastel end |
Class Method Details
.emergency_restore_all ⇒ Object
Trap-safe variant of restore_all: iterates a snapshot of the registry without @registry_mutex, which the main thread may hold when the signal arrives (see review P0-5).
44 45 46 47 48 49 50 |
# File 'lib/tty/command/window/coordinator.rb', line 44 def emergency_restore_all # .values (not each_value): iterating the live hash could raise # if the main thread mutates the registry mid-trap. @registry.values.each(&:emergency_restore) # rubocop:disable Style/HashEachMethods rescue StandardError nil end |
.for(output, width: nil) ⇒ Coordinator
Coordinator bound to the given output IO (one per IO).
28 29 30 31 32 |
# File 'lib/tty/command/window/coordinator.rb', line 28 def for(output, width: nil) @registry_mutex.synchronize do @registry[output] ||= new(output: output, width_override: width) end end |
.restore_all ⇒ Object
Emergency cleanup for at_exit: restore the cursor everywhere.
35 36 37 38 39 |
# File 'lib/tty/command/window/coordinator.rb', line 35 def restore_all @registry_mutex.synchronize do @registry.each_value(&:emergency_restore) end end |
Instance Method Details
#attach_session(block, session) ⇒ Object
Attach a child session to an already-registered block so signal forwarding and PTY resize reach it. Used by step windows, whose blocks outlive any single command.
134 135 136 |
# File 'lib/tty/command/window/coordinator.rb', line 134 def attach_session(block, session) @mutex.synchronize { @sessions[block] = session } end |
#color? ⇒ Boolean
Returns whether colored output is enabled.
157 158 159 160 161 |
# File 'lib/tty/command/window/coordinator.rb', line 157 def color? return false if ENV.key?("NO_COLOR") Window.assume_tty || (@output.respond_to?(:tty?) && @output.tty?) end |
#detach_session(block) ⇒ Object
Detach the session attached via #attach_session.
141 142 143 |
# File 'lib/tty/command/window/coordinator.rb', line 141 def detach_session(block) @mutex.synchronize { @sessions.delete(block) } end |
#emergency_restore ⇒ Object
at_exit safety net: show the cursor if we died mid-paint.
164 165 166 167 168 |
# File 'lib/tty/command/window/coordinator.rb', line 164 def emergency_restore @output.write(SHOW_CURSOR) if @active && @output.respond_to?(:write) && !@output.closed? rescue IOError, Errno::EBADF, Errno::EPIPE nil end |
#finalize(block) ⇒ Object
Called by the runner when a block's command finished. Repaints and, for dump-on-failure blocks, replaces the block with its full history as permanent scrolled output. Drains the region when every block is done.
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 |
# File 'lib/tty/command/window/coordinator.rb', line 100 def finalize(block) drained = false @mutex.synchronize do next unless @active if block.dump_on_finalize? wipe_region @blocks.delete(block) write_permanent(block.dump_text(@pastel)) elsif block.retire_on_finalize? wipe_region @blocks.delete(block) write_permanent(block.summary_line(width: width, pastel: @pastel)) end # Drop the session unconditionally: the child has been reaped by # Runner and its PID is no longer safe to signal. Leaving stale # entries in @sessions risks INT/TERM delivery to a recycled PID # (see follow-up review P0-2). @sessions.delete(block) paint if @blocks.all?(&:done?) drain drained = true end end join_render_thread if drained end |
#mark_dirty ⇒ Object
Request a repaint on the next frame (cheap, lock-free).
146 147 148 |
# File 'lib/tty/command/window/coordinator.rb', line 146 def mark_dirty @dirty = true end |
#register(block, child_session: nil) ⇒ Object
Add a block to the stack and start rendering if needed.
87 88 89 90 91 92 93 94 |
# File 'lib/tty/command/window/coordinator.rb', line 87 def register(block, child_session: nil) @mutex.synchronize do activate unless @active @blocks << block @sessions[block] = child_session if child_session paint end end |
#width ⇒ Integer
Returns current terminal width in columns.
70 71 72 73 74 75 76 77 78 79 80 |
# File 'lib/tty/command/window/coordinator.rb', line 70 def width return @width_override if @width_override if @output.respond_to?(:winsize) && @output.respond_to?(:tty?) && @output.tty? cols = @output.winsize[1] return cols if cols.positive? end (ENV["COLUMNS"] || "80").to_i.clamp(20, 1000) rescue Errno::ENOTTY, Errno::EBADF, IOError 80 end |
#winch! ⇒ Object
Trap-context WINCH notification; actual work happens on the render thread.
152 153 154 |
# File 'lib/tty/command/window/coordinator.rb', line 152 def winch! @winch = true end |