Class: TTY::Command::Window::Coordinator

Inherits:
Object
  • Object
show all
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

Class Method Summary collapse

Instance Method Summary collapse

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

#outputObject (readonly)

Returns the value of attribute output.



53
54
55
# File 'lib/tty/command/window/coordinator.rb', line 53

def output
  @output
end

#pastelObject (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_allObject

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).

Parameters:

  • output (IO)
  • width (Integer, nil) (defaults to: nil)

    fixed width override

Returns:



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_allObject

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

#color?Boolean

Returns whether colored output is enabled.

Returns:

  • (Boolean)

    whether colored output is enabled



136
137
138
139
140
# File 'lib/tty/command/window/coordinator.rb', line 136

def color?
  return false if ENV.key?("NO_COLOR")

  Window.assume_tty || (@output.respond_to?(:tty?) && @output.tty?)
end

#emergency_restoreObject

at_exit safety net: show the cursor if we died mid-paint.



143
144
145
146
147
# File 'lib/tty/command/window/coordinator.rb', line 143

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
# 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))
    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_dirtyObject

Request a repaint on the next frame (cheap, lock-free).



125
126
127
# File 'lib/tty/command/window/coordinator.rb', line 125

def mark_dirty
  @dirty = true
end

#register(block, child_session: nil) ⇒ Object

Add a block to the stack and start rendering if needed.

Parameters:

  • block (Block)
  • child_session (ChildSession, nil) (defaults to: nil)

    used for signal forwarding and PTY winsize updates; may be nil for test doubles.



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

#widthInteger

Returns current terminal width in columns.

Returns:

  • (Integer)

    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.



131
132
133
# File 'lib/tty/command/window/coordinator.rb', line 131

def winch!
  @winch = true
end