Class: TTY::Command::Window::InputRouter
- Inherits:
-
Object
- Object
- TTY::Command::Window::InputRouter
- 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_indexhold@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 -gsnapshot; see review H4/P0-5.
Defined Under Namespace
Classes: Entry
Constant Summary collapse
- FOCUS_KEY =
Ctrl-O
"\x0f"
Class Attribute Summary collapse
-
.saved_termios ⇒ Object
Returns the value of attribute saved_termios.
Class Method Summary collapse
-
.attach(block, session, coordinator) ⇒ Object
Attach an interactive block; starts the router on first attach.
- .capture_termios ⇒ Object
-
.detach(block) ⇒ Object
Detach a block; stops the router when none remain.
-
.emergency_restore ⇒ Object
Restore the terminal to whatever mode it was in before we switched it into raw.
-
.install_emergency_hooks ⇒ Object
Install the emergency-restore hooks (at_exit + SIGTERM/SIGHUP) exactly once per process.
Instance Method Summary collapse
- #add(block, session, coordinator) ⇒ Object
-
#empty? ⇒ Boolean
True when no blocks are attached.
-
#initialize ⇒ InputRouter
constructor
A new instance of InputRouter.
- #remove(block) ⇒ Object
-
#shutdown ⇒ Object
Stop the router thread, join it, close pipes.
Constructor Details
#initialize ⇒ InputRouter
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_termios ⇒ Object
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.
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_termios ⇒ Object
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_restore ⇒ Object
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_hooks ⇒ Object
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.
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 |
#shutdown ⇒ Object
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 |