Class: TTY::Command::Window::InputRouter

Inherits:
Object
  • Object
show all
Defined in:
lib/tty/command/window/input_router.rb

Overview

Routes keyboard input to interactive windows.

While at least one interactive block is attached, the process's stdin is switched into raw mode and every keystroke is forwarded to the focused block's PTY. Ctrl-O cycles focus between interactive blocks.

Note: in raw mode Ctrl-C is delivered to the focused child as a byte (0x03), not as SIGINT to the host process.

Thread-ownership:

  • The class methods InputRouter.attach/InputRouter.detach are serialized by the class mutex and provide the process-global singleton contract.
  • The instance methods that mutate or read @entries / @focus_index hold @state_mutex; the router thread takes the same mutex when it forwards or cycles focus, so a concurrent add/remove cannot race the reader (see review H7).
  • On emergency termination (SIGTERM/SIGHUP/at_exit) the terminal is restored from a saved stty -g snapshot; see review H4/P0-5.

Defined Under Namespace

Classes: Entry

Constant Summary collapse

FOCUS_KEY =

Ctrl-O

"\x0f"

Class Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeInputRouter

Returns a new instance of InputRouter.



113
114
115
116
117
118
119
120
# File 'lib/tty/command/window/input_router.rb', line 113

def initialize
  @entries = [] # [Entry, ...]
  @focus_index = 0
  @wake_read, @wake_write = IO.pipe
  @running = false
  @thread = nil
  @state_mutex = Mutex.new
end

Class Attribute Details

.saved_termiosObject

Returns the value of attribute saved_termios.



40
41
42
# File 'lib/tty/command/window/input_router.rb', line 40

def saved_termios
  @saved_termios
end

Class Method Details

.attach(block, session, coordinator) ⇒ Object

Attach an interactive block; starts the router on first attach.

Parameters:

  • block (Block)

    the view that owns the focus flag

  • session (ChildSession)

    where keystrokes are written

  • coordinator (Coordinator)

    repaint sink



47
48
49
50
51
52
53
54
# File 'lib/tty/command/window/input_router.rb', line 47

def attach(block, session, coordinator)
  return unless $stdin.tty?

  @mutex.synchronize do
    @instance ||= new
    @instance.add(block, session, coordinator)
  end
end

.capture_termiosObject



105
106
107
108
109
110
# File 'lib/tty/command/window/input_router.rb', line 105

def capture_termios
  out = `stty -g < /dev/tty 2>/dev/null`.chomp
  out.empty? ? nil : out
rescue StandardError
  nil
end

.detach(block) ⇒ Object

Detach a block; stops the router when none remain.

Holds @mutex across shutdown so a concurrent attach cannot construct a replacement router while the old one is still joining its thread and closing its wake pipes — which would race the class-level @saved_termios slot and let emergency_restore wipe the new router's snapshot (follow-up review P1-5).



64
65
66
67
68
69
70
71
72
73
74
# File 'lib/tty/command/window/input_router.rb', line 64

def detach(block)
  @mutex.synchronize do
    next unless @instance

    @instance.remove(block)
    if @instance.empty?
      @instance.shutdown
      @instance = nil
    end
  end
end

.emergency_restoreObject

Restore the terminal to whatever mode it was in before we switched it into raw. Safe to call from at_exit or a signal handler. Idempotent.



79
80
81
82
83
84
85
86
87
# File 'lib/tty/command/window/input_router.rb', line 79

def emergency_restore
  saved = @saved_termios
  return unless saved && !saved.empty?

  @saved_termios = nil
  system("stty", saved, in: "/dev/tty", err: File::NULL, out: File::NULL)
rescue StandardError
  nil
end

.install_emergency_hooksObject

Install the emergency-restore hooks (at_exit + SIGTERM/SIGHUP) exactly once per process.



91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/tty/command/window/input_router.rb', line 91

def install_emergency_hooks
  @mutex.synchronize do
    return if @emergency_installed

    @emergency_installed = true
    at_exit { InputRouter.emergency_restore }
    @termios_sub = ->(_sig) { InputRouter.emergency_restore }
    TrapManager.subscribe("TERM", @termios_sub)
    TrapManager.subscribe("HUP", @termios_sub)
    TrapManager.subscribe("INT", @termios_sub)
  end
end

Instance Method Details

#add(block, session, coordinator) ⇒ Object



122
123
124
125
126
127
128
129
# File 'lib/tty/command/window/input_router.rb', line 122

def add(block, session, coordinator)
  @state_mutex.synchronize do
    @entries << Entry.new(block, session, coordinator)
    refocus_locked(@entries.length - 1) if @entries.length == 1
  end
  start unless @running
  repaint_all
end

#empty?Boolean

Returns true when no blocks are attached.

Returns:

  • (Boolean)

    true when no blocks are attached



146
147
148
# File 'lib/tty/command/window/input_router.rb', line 146

def empty?
  @state_mutex.synchronize { @entries.empty? }
end

#remove(block) ⇒ Object



131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/tty/command/window/input_router.rb', line 131

def remove(block)
  removed = false
  @state_mutex.synchronize do
    index = @entries.index { |entry| entry.block.equal?(block) }
    next unless index

    @entries.delete_at(index)
    block.focused = false
    removed = true
    refocus_locked(@focus_index % @entries.length) unless @entries.empty?
  end
  repaint_all if removed && !empty?
end

#shutdownObject

Stop the router thread, join it, close pipes. Idempotent.



151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/tty/command/window/input_router.rb', line 151

def shutdown
  @running = false
  begin
    @wake_write.write("x")
  rescue IOError, Errno::EPIPE
    nil
  end
  thread = @thread
  thread&.join(1)
  @thread = nil
  [@wake_write, @wake_read].each do |io|
    io.close
  rescue IOError
    nil
  end
  self.class.emergency_restore
end