Class: Muxr::InputHandler
- Inherits:
-
Object
- Object
- Muxr::InputHandler
- Defined in:
- lib/muxr/input_handler.rb
Overview
Translates raw keystrokes into either commands or pane input. Two top-level modes:
:normal Default. Single-key bindings (hjkl navigation, t/g/m for
layouts, c/K for create/kill, etc.) act directly without
any prefix. `i` drops into passthrough.
:passthrough Historical mode: every key is forwarded to the focused
pane unless prefixed by Ctrl-a. `Ctrl-a Esc` returns to
normal mode.
Plus the sub-states pre-existing from before modes existed:
:prefix, :command, :confirm_quit, :confirm_close, :help, :scrollback,
:search, :selection.
One-shot sub-states (prefix, command, confirm_quit, help) return to finish. :scrollback and :selection also return to @base_mode so that exiting back from a scroll/yank lands you back in passthrough if that’s where you came from.
Scrollback is effectively pane-bound. Ctrl-a is honored from inside :scrollback and :selection — it drops into :prefix (with @prefix_return
:scrollback) so a pane switch keeps you in scrollback on the pane you
move to, while the pane you left keeps its own scroll position. Coming the other way, the Application re-enters scrollback whenever you focus a pane that was left scrolled back. ‘i` from scrollback drops to insert (passthrough) without snapping to the bottom; only `q`/Esc returns the pane to the live bottom.
Constant Summary collapse
- PREFIX =
Ctrl-a
"\x01".freeze
- NORMAL_BINDINGS =
Single-key bindings in normal mode. Same actions as their Ctrl-a- prefixed counterparts in passthrough, just without the prefix. Value forms:
:symbol → @app.public_send(:symbol) [:symbol, *args] → @app.public_send(:symbol, *args) { "c" => :new_pane, "x" => :request_close, "t" => [:set_layout, :tall], "w" => [:set_layout, :wide], "g" => [:set_layout, :grid], "m" => [:set_layout, :monocle], "|" => [:set_layout, :columns], "-" => [:set_layout, :rows], "f" => [:set_layout, :spiral], "e" => [:set_layout, :centered], "S" => [:set_layout, :stack], "\t" => :cycle_layout, "\r" => :promote_master, "\n" => :promote_master, "h" => [:focus_direction, :left], "j" => [:focus_direction, :down], "k" => [:focus_direction, :up], "l" => [:focus_direction, :right], "H" => [:move_direction, :left], "J" => [:move_direction, :down], "K" => [:move_direction, :up], "L" => [:move_direction, :right], "a" => :focus_last, "r" => :refresh_focused, "~" => :toggle_drawer, "C" => :toggle_claude_drawer, "P" => :toggle_private_focused, "d" => :detach, "?" => :show_help, "q" => :quit_immediate, "s" => :enter_scrollback, "]" => :paste_from_buffer }.freeze
- PREFIX_BINDINGS =
{ "c" => :new_pane, "n" => :focus_next, "p" => :focus_prev, "a" => :focus_last, "r" => :refresh_focused, "x" => :request_close, "\t" => :cycle_layout, "\r" => :promote_master, "\n" => :promote_master, "~" => :toggle_drawer, "C" => :toggle_claude_drawer, "P" => :toggle_private_focused, "d" => :detach, "?" => :show_help, "q" => :quit_immediate, "[" => :enter_scrollback, "]" => :paste_from_buffer }.freeze
- SCROLLBACK_BINDINGS =
{ "j" => :line_forward, "k" => :line_back, "\x04" => :half_forward, # Ctrl-d "\x15" => :half_back, # Ctrl-u "d" => :half_forward, "u" => :half_back, "\x06" => :full_forward, # Ctrl-f "\x02" => :full_back, # Ctrl-b "f" => :full_forward, "b" => :full_back, " " => :full_forward, "g" => :top, "G" => :bottom }.freeze
- SCROLLBACK_CSI =
CSI sequences (arrow / page keys) recognized in scrollback mode. Built for terminal raw-mode emission: arrow keys come through as ESC ‘[` followed by a single final letter, PageUp/PageDown as ESC `[5~` / `[6~`. Lookahead in #feed peels these off as one chunk so a bare ESC still exits scrollback the way it always has.
{ "\e[A" => :line_back, # Up "\e[B" => :line_forward, # Down "\e[5~" => :half_back, # PageUp "\e[6~" => :half_forward, # PageDown "\e[H" => :top, # Home "\e[F" => :bottom # End }.freeze
- SCROLLBACK_EXITS =
q, Esc, Ctrl-c
["q", "\e", "\x03"].freeze
- SELECTION_BINDINGS =
{ "h" => :left, "l" => :right, "j" => :down, "k" => :up, "0" => :line_start, "$" => :line_end, "^" => :line_first_nonblank, "g" => :top, "G" => :bottom, "H" => :screen_top, "M" => :screen_middle, "L" => :screen_bottom, "w" => :word_forward, "W" => :word_forward_big, "e" => :word_end, "E" => :word_end_big, # `b` is vim word-back here; the tmux-style page-back alias lives on Ctrl-b. "b" => :word_backward, "B" => :word_backward_big, "\x04" => :half_down, # Ctrl-d "\x15" => :half_up, # Ctrl-u "d" => :half_down, "u" => :half_up, "\x06" => :full_down, # Ctrl-f "\x02" => :full_up, # Ctrl-b "f" => :full_down # NOTE: space is intentionally absent here — it's a top-level toggle # for linear selection (see handle_selection_input), mirroring vim's # `v` so the right thumb has a one-key way to anchor/release. }.freeze
- SELECTION_YANK =
["\r", "\n", "y"].freeze
- SELECTION_CANCEL =
q, Esc, Ctrl-c
["q", "\e", "\x03"].freeze
- DIGIT_RE =
/\A[1-9]\z/.freeze
Instance Attribute Summary collapse
-
#base_mode ⇒ Object
readonly
Returns the value of attribute base_mode.
-
#command_buffer ⇒ Object
readonly
Returns the value of attribute command_buffer.
-
#search_buffer ⇒ Object
readonly
Returns the value of attribute search_buffer.
-
#search_direction ⇒ Object
readonly
Returns the value of attribute search_direction.
-
#state ⇒ Object
readonly
Returns the value of attribute state.
Instance Method Summary collapse
- #cancel ⇒ Object
- #enter_confirm_close ⇒ Object
- #enter_confirm_quit ⇒ Object
- #enter_help_mode ⇒ Object
-
#enter_idle_mode ⇒ Object
Exit a sub-state (scrollback, selection-yank) and resume the mode the user was in before they entered scrollback.
-
#enter_normal_mode ⇒ Object
Return to normal mode.
-
#enter_passthrough_mode ⇒ Object
Drop into passthrough — every key reaches the focused pane until the user issues Ctrl-a Esc.
- #enter_scrollback_mode ⇒ Object
- #enter_search_mode(direction: :forward) ⇒ Object
- #enter_selection_mode ⇒ Object
- #feed(data) ⇒ Object
-
#initialize(app) ⇒ InputHandler
constructor
A new instance of InputHandler.
Constructor Details
#initialize(app) ⇒ InputHandler
Returns a new instance of InputHandler.
165 166 167 168 169 170 171 172 173 174 175 176 177 |
# File 'lib/muxr/input_handler.rb', line 165 def initialize(app) @app = app @state = :normal @base_mode = :normal @command_buffer = +"" @search_buffer = +"" @search_direction = :forward # When the prefix state is entered from scrollback/selection (Ctrl-a), # this records :scrollback so that a pane switch lands you back in # scrollback on the newly-focused pane instead of dropping to the base # mode. nil means "use @base_mode" (the normal passthrough behavior). @prefix_return = nil end |
Instance Attribute Details
#base_mode ⇒ Object (readonly)
Returns the value of attribute base_mode.
163 164 165 |
# File 'lib/muxr/input_handler.rb', line 163 def base_mode @base_mode end |
#command_buffer ⇒ Object (readonly)
Returns the value of attribute command_buffer.
163 164 165 |
# File 'lib/muxr/input_handler.rb', line 163 def command_buffer @command_buffer end |
#search_buffer ⇒ Object (readonly)
Returns the value of attribute search_buffer.
163 164 165 |
# File 'lib/muxr/input_handler.rb', line 163 def search_buffer @search_buffer end |
#search_direction ⇒ Object (readonly)
Returns the value of attribute search_direction.
163 164 165 |
# File 'lib/muxr/input_handler.rb', line 163 def search_direction @search_direction end |
#state ⇒ Object (readonly)
Returns the value of attribute state.
163 164 165 |
# File 'lib/muxr/input_handler.rb', line 163 def state @state end |
Instance Method Details
#cancel ⇒ Object
286 287 288 289 |
# File 'lib/muxr/input_handler.rb', line 286 def cancel @state = @base_mode @command_buffer = +"" end |
#enter_confirm_close ⇒ Object
246 247 248 |
# File 'lib/muxr/input_handler.rb', line 246 def enter_confirm_close @state = :confirm_close end |
#enter_confirm_quit ⇒ Object
242 243 244 |
# File 'lib/muxr/input_handler.rb', line 242 def enter_confirm_quit @state = :confirm_quit end |
#enter_help_mode ⇒ Object
238 239 240 |
# File 'lib/muxr/input_handler.rb', line 238 def enter_help_mode @state = :help end |
#enter_idle_mode ⇒ Object
Exit a sub-state (scrollback, selection-yank) and resume the mode the user was in before they entered scrollback. Preserves @base_mode so a passthrough → scrollback → exit round-trip lands back in passthrough.
282 283 284 |
# File 'lib/muxr/input_handler.rb', line 282 def enter_idle_mode @state = @base_mode end |
#enter_normal_mode ⇒ Object
Return to normal mode. Used by the ‘Ctrl-a Esc` binding from passthrough — explicitly resets @base_mode so the user genuinely leaves passthrough.
274 275 276 277 |
# File 'lib/muxr/input_handler.rb', line 274 def enter_normal_mode @state = :normal @base_mode = :normal end |
#enter_passthrough_mode ⇒ Object
Drop into passthrough — every key reaches the focused pane until the user issues Ctrl-a Esc.
266 267 268 269 |
# File 'lib/muxr/input_handler.rb', line 266 def enter_passthrough_mode @state = :passthrough @base_mode = :passthrough end |
#enter_scrollback_mode ⇒ Object
250 251 252 |
# File 'lib/muxr/input_handler.rb', line 250 def enter_scrollback_mode @state = :scrollback end |
#enter_search_mode(direction: :forward) ⇒ Object
254 255 256 257 258 |
# File 'lib/muxr/input_handler.rb', line 254 def enter_search_mode(direction: :forward) @state = :search @search_direction = direction @search_buffer = +"" end |
#enter_selection_mode ⇒ Object
260 261 262 |
# File 'lib/muxr/input_handler.rb', line 260 def enter_selection_mode @state = :selection end |
#feed(data) ⇒ Object
179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 |
# File 'lib/muxr/input_handler.rb', line 179 def feed(data) remaining = data until remaining.empty? if @state == :passthrough # Fast path: batch everything up to the next Ctrl-a as one chunk so # a large paste doesn't turn into one PTY write per byte. PREFIX is # single-byte ASCII (\x01) and never appears mid-UTF-8. idx = remaining.index(PREFIX) if idx.nil? @app.send_to_focused(remaining) return end @app.send_to_focused(remaining[0...idx]) if idx > 0 @state = :prefix remaining = remaining[(idx + 1)..] || "" next end # Multi-byte CSI lookahead for scrollback / search: arrow / page # keys arrive as `\e[<final>` and would otherwise trip the # bare-Esc-exits behavior. In :scrollback we map them to scroll # actions; in :search we silently consume them so a stray arrow # doesn't kick the user out of the prompt. An incomplete `\e[…` # (rare in raw-mode TTY) falls through and the bare `\e` exits as # before. if (@state == :scrollback || @state == :search) && remaining.start_with?("\e[") consumed = consume_csi_escape(remaining) if consumed > 0 remaining = remaining[consumed..] || "" next end end ch = remaining[0] remaining = remaining[1..] || "" case @state when :normal handle_normal(ch) when :help @app.dismiss_help @state = @base_mode when :confirm_quit handle_confirm_quit(ch) when :confirm_close handle_confirm_close(ch) when :prefix handle_prefix(ch) when :command handle_command_input(ch) when :scrollback handle_scrollback_input(ch) when :search handle_search_input(ch) when :selection handle_selection_input(ch) end end end |