Class: Muxr::InputHandler

Inherits:
Object
  • Object
show all
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, :help, :scrollback, :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.

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,
  "K"  => :close_focused,
  "t"  => [:set_layout, :tall],
  "g"  => [:set_layout, :grid],
  "m"  => [:set_layout, :monocle],
  "\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],
  "a"  => :focus_last,
  "~"  => :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,
  "K"    => :close_focused,
  "\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_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

Instance Method Summary collapse

Constructor Details

#initialize(app) ⇒ InputHandler

Returns a new instance of InputHandler.



129
130
131
132
133
134
# File 'lib/muxr/input_handler.rb', line 129

def initialize(app)
  @app = app
  @state = :normal
  @base_mode = :normal
  @command_buffer = +""
end

Instance Attribute Details

#base_modeObject (readonly)

Returns the value of attribute base_mode.



127
128
129
# File 'lib/muxr/input_handler.rb', line 127

def base_mode
  @base_mode
end

#command_bufferObject (readonly)

Returns the value of attribute command_buffer.



127
128
129
# File 'lib/muxr/input_handler.rb', line 127

def command_buffer
  @command_buffer
end

#stateObject (readonly)

Returns the value of attribute state.



127
128
129
# File 'lib/muxr/input_handler.rb', line 127

def state
  @state
end

Instance Method Details

#cancelObject



214
215
216
217
# File 'lib/muxr/input_handler.rb', line 214

def cancel
  @state = @base_mode
  @command_buffer = +""
end

#enter_confirm_quitObject



180
181
182
# File 'lib/muxr/input_handler.rb', line 180

def enter_confirm_quit
  @state = :confirm_quit
end

#enter_help_modeObject



176
177
178
# File 'lib/muxr/input_handler.rb', line 176

def enter_help_mode
  @state = :help
end

#enter_idle_modeObject

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.



210
211
212
# File 'lib/muxr/input_handler.rb', line 210

def enter_idle_mode
  @state = @base_mode
end

#enter_normal_modeObject

Return to normal mode. Used by the ‘Ctrl-a Esc` binding from passthrough — explicitly resets @base_mode so the user genuinely leaves passthrough.



202
203
204
205
# File 'lib/muxr/input_handler.rb', line 202

def enter_normal_mode
  @state = :normal
  @base_mode = :normal
end

#enter_passthrough_modeObject

Drop into passthrough — every key reaches the focused pane until the user issues Ctrl-a Esc.



194
195
196
197
# File 'lib/muxr/input_handler.rb', line 194

def enter_passthrough_mode
  @state = :passthrough
  @base_mode = :passthrough
end

#enter_scrollback_modeObject



184
185
186
# File 'lib/muxr/input_handler.rb', line 184

def enter_scrollback_mode
  @state = :scrollback
end

#enter_selection_modeObject



188
189
190
# File 'lib/muxr/input_handler.rb', line 188

def enter_selection_mode
  @state = :selection
end

#feed(data) ⇒ Object



136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/muxr/input_handler.rb', line 136

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

    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 :prefix
      handle_prefix(ch)
    when :command
      handle_command_input(ch)
    when :scrollback
      handle_scrollback_input(ch)
    when :selection
      handle_selection_input(ch)
    end
  end
end