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

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

Parameters:



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.

Returns:

  • (Boolean)

    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.

Parameters:



141
142
143
# File 'lib/tty/command/window/coordinator.rb', line 141

def detach_session(block)
  @mutex.synchronize { @sessions.delete(block) }
end

#emergency_restoreObject

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_dirtyObject

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.

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.



152
153
154
# File 'lib/tty/command/window/coordinator.rb', line 152

def winch!
  @winch = true
end