Class: PWN::Plugins::REPL::PWNMultiLineInput
- Inherits:
-
Object
- Object
- PWN::Plugins::REPL::PWNMultiLineInput
- Defined in:
- lib/pwn/plugins/repl.rb
Overview
Custom input handler for pwn-ai and pwn-asm to support multi-line submissions:
- Use only SHIFT+ENTER to insert a newline (continue editing).
- Plain ENTER submits the full (possibly multi-line) buffer.
- Multi-line pastes are supported (Reline handles \n in buffer; submit with ENTER). Strict SHIFT+ENTER only — no Ctrl+J, Alt-Enter, or other fallbacks (per requirements).
Constant Summary collapse
- SHIFT_ENTER_SEQS =
SHIFT+ENTER escape sequences (byte arrays). These are terminal-dependent. Listed common ones for xterm, VTE (terminator), kitty, wezterm, etc. (with modifyOtherKeys / extended-keys enabled).
For tmux + terminator (or similar):
In ~/.tmux.conf (then `tmux kill-server` + new session): set -s extended-keys on set -g xterm-keys on Use TERM=xterm-256color (or equivalent that supports the CSI) in your terminal profile.The bindings make matching sequences produce :key_newline (insert \n without submit).
If after typing text + SHIFT+ENTER it still submits instead of newline:
1. Apply the tmux.conf + TERM changes above and fully restart tmux. 2. In your *real* terminal (the one running `pwn`), run a capture script from /tmp ONLY: ruby /tmp/capture_keys.rb (Debugging scripts must live in /tmp per user rule; never commit them to /opt/pwn.) 3. Paste the exact bytes array for the SHIFT+ENTER press here so it can be added to the list. [ [27, 91, 49, 51, 59, 50, 126], # \e[13;2~ [27, 91, 50, 55, 59, 50, 59, 49, 51, 126], # \e[27;2;13~ [27, 91, 49, 51, 59, 50, 117], # \e[13;2u (CSI u) [27, 91, 50, 55, 59, 50, 59, 49, 51, 117], # \e[27;2;13u [27, 91, 49, 59, 50, 126], # \e[1;2~ [27, 13], # \e\r (ESC+CR variant) [27, 10], # \e\n (ESC+LF variant) [27, 91, 13, 59, 50, 126], # \e[13;2~ alt numeric [27, 91, 49, 59, 50, 117], # \e[1;2u [27, 91, 50, 55, 59, 50, 13, 126], # \e[27;2;13~ variant [27, 79, 77] # \eOM (application-keypad Enter; some emulators emit this for S-Enter) ].freeze
- ENABLE_EXTENDED_KEYS =
CSI sequences that ask the terminal to start/stop encoding Shift+Enter (and other modified keys) distinctly from plain Enter. Without one of these active, most emulators send the SAME byte (0x0D) for both, so SHIFT_ENTER_SEQS can never match.
\e[>4;1m / \e[>4;0m xterm modifyOtherKeys on/off (level 1 — disambiguates Shift+Enter without altering Ctrl-C). xterm, VTE/Terminator, iTerm2, Konsole. tmux ≥3.2 with `extended-keys on` honours this request and re-encodes as CSI-u to the inner app. \e[>1u / \e[<u kitty keyboard protocol push/pop, flags=1 "disambiguate escape codes". kitty, wezterm, foot, ghostty, alacritty, recent tmux.Emitting both is harmless on terminals that support neither — they're DEC-private CSIs and get silently ignored.
"\e[>4;1m\e[>1u"- DISABLE_EXTENDED_KEYS =
"\e[<u\e[>4;0m"
Instance Attribute Summary collapse
-
#line_buffer ⇒ Object
readonly
Returns the value of attribute line_buffer.
Instance Method Summary collapse
-
#ensure_tmux_extended_keys ⇒ Object
tmux gates modifyOtherKeys / kitty-keyboard requests behind its
extended-keysserver option. -
#initialize(pry_instance) ⇒ PWNMultiLineInput
constructor
A new instance of PWNMultiLineInput.
-
#install_shift_enter_bindings ⇒ Object
Register SHIFT+ENTER → :key_newline on Reline's default keymaps.
- #readline(prompt) ⇒ Object
-
#reline_config ⇒ Object
Reline ≤ 0.5.x exposed a top-level
Reline.configdelegator. -
#tty? ⇒ Boolean
Compatibility with Pry input expectations.
- #winsize ⇒ Object
Constructor Details
#initialize(pry_instance) ⇒ PWNMultiLineInput
Returns a new instance of PWNMultiLineInput.
77 78 79 80 81 82 |
# File 'lib/pwn/plugins/repl.rb', line 77 def initialize(pry_instance) @line_buffer = '' pry_instance.config.pwn_ai_original_input = Pry.input ensure_tmux_extended_keys install_shift_enter_bindings end |
Instance Attribute Details
#line_buffer ⇒ Object (readonly)
Returns the value of attribute line_buffer.
23 24 25 |
# File 'lib/pwn/plugins/repl.rb', line 23 def line_buffer @line_buffer end |
Instance Method Details
#ensure_tmux_extended_keys ⇒ Object
tmux gates modifyOtherKeys / kitty-keyboard requests behind its
extended-keys server option. When off (the shipped default
on many distros / older ~/.tmux.conf), tmux silently drops the
ENABLE_EXTENDED_KEYS CSI we emit in #readline and forwards plain
0x0D for BOTH Enter and Shift+Enter — SHIFT_ENTER_SEQS can then
never match and Shift+Enter "still just submits".
Detect tmux via $TMUX, read the current server option, and flip it
to on (NOT always) so tmux honours the per-read enable/disable
we send around Reline.readmultiline. on is scoped: tmux only
encodes extended keys while the inner app is requesting them, so
this does not affect other panes or the main pwn REPL.
Verified on tmux 3.6b: extended-keys on + \e[>4;1m → S-Enter is
delivered as \e[27;2;13~ (matches SHIFT_ENTER_SEQS).
110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 |
# File 'lib/pwn/plugins/repl.rb', line 110 def ensure_tmux_extended_keys return if self.class.instance_variable_get(:@tmux_extkeys_checked) self.class.instance_variable_set(:@tmux_extkeys_checked, true) return if ENV['TMUX'].to_s.empty? # (1) Inner side: tmux → app. `extended-keys on` makes tmux honour the # ENABLE_EXTENDED_KEYS request we emit in #readline and re-encode # S-Enter to the pane as CSI 27;2;13~ / CSI 13;2u. cur = `tmux show -sv extended-keys 2>/dev/null`.to_s.strip unless %w[on always].include?(cur) if system('tmux', 'set', '-s', 'extended-keys', 'on', out: File::NULL, err: File::NULL) warn '[pwn] tmux `extended-keys` was off; auto-enabled (server scope) so SHIFT+ENTER is distinguishable from ENTER.' warn '[pwn] Persist it: add `set -s extended-keys on` to ~/.tmux.conf' else warn '[pwn] tmux `extended-keys` is off and could not be enabled; SHIFT+ENTER will behave like ENTER.' warn '[pwn] Fix: run `tmux set -s extended-keys on` (and add `set -s extended-keys on` to ~/.tmux.conf).' end end # (2) Outer side: terminal → tmux. tmux only ASKS the outer terminal # to encode S-Enter distinctly (sends `\e[>4;2m` at attach) if the # client tty has the `extkeys` feature. That comes from the # `terminal-features` server option matched against the client's # $TERM at attach time. No match ⇒ outer emulator keeps sending # 0x0D for BOTH Enter and Shift+Enter ⇒ tmux can't disambiguate ⇒ # step (1) is moot. Add it for common outer TERMs (and tmux* to # cover `alias tmux='TERM=tmux-256color tmux'` and nested tmux). tf = `tmux show -sv terminal-features 2>/dev/null`.to_s unless tf.include?('extkeys') %w[xterm* tmux* screen*].each do |pat| system('tmux', 'set', '-as', 'terminal-features', "#{pat}:extkeys", out: File::NULL, err: File::NULL) end warn '[pwn] Added `extkeys` to tmux terminal-features (xterm*/tmux*/screen*) so tmux requests extended keys from the OUTER terminal.' warn "[pwn] Persist it: add `set -as terminal-features 'xterm*:extkeys'` (and tmux*/screen*) to ~/.tmux.conf" end # (3) terminal-features is evaluated at CLIENT ATTACH time. If the # current client attached before `extkeys` was present, tmux never # sent the enable CSI to the outer terminal. Detect and warn. feats = `tmux display -p '\#{client_termfeatures}' 2>/dev/null`.to_s return if feats.include?('extkeys') warn '[pwn] This tmux client attached before `extkeys` was configured; the outer terminal is still sending plain 0x0D for SHIFT+ENTER.' warn '[pwn] Fix: detach (prefix + d) and reattach (`tmux attach -t <session>`) so tmux re-negotiates extended keys with the terminal.' rescue StandardError => e warn "[pwn] ensure_tmux_extended_keys: #{e.class}: #{e.}" end |
#install_shift_enter_bindings ⇒ Object
Register SHIFT+ENTER → :key_newline on Reline's default keymaps.
IMPORTANT: do NOT use add_oneshot_key_binding for this. Reline's LineEditor#input_key calls reset_oneshot_key_bindings on EVERY keystroke (it's designed for dialog trap-keys = "next keypress only"), so oneshot bindings are wiped the moment the user types their first character — Shift+Enter then falls through as an unrecognised CSI and is silently swallowed. Default-keymap bindings persist for the life of the Config object.
Scoping is handled by the input-handler swap, not the binding lifetime: outside pwn-ai/pwn-asm, Pry uses its own input, PWNMultiLineInput#readline never runs, ENABLE_EXTENDED_KEYS is never emitted, the terminal sends plain 0x0D for Shift+Enter, and these bindings never match. So registering once at construction is safe.
174 175 176 177 178 179 180 181 182 183 184 |
# File 'lib/pwn/plugins/repl.rb', line 174 def install_shift_enter_bindings return if self.class.instance_variable_get(:@shift_enter_installed) cfg = reline_config %i[emacs vi_insert].each do |keymap| SHIFT_ENTER_SEQS.each do |seq| cfg.add_default_key_binding_by_keymap(keymap, seq, :key_newline) end end self.class.instance_variable_set(:@shift_enter_installed, true) end |
#readline(prompt) ⇒ Object
186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 |
# File 'lib/pwn/plugins/repl.rb', line 186 def readline(prompt) # Ask the terminal to encode Shift+Enter distinctly from Enter for # the duration of this read. Without this, most emulators send 0x0D # for both and SHIFT_ENTER_SEQS can never match. Reset in `ensure`. tty = $stdout.respond_to?(:tty?) && $stdout.tty? if tty $stdout.write(ENABLE_EXTENDED_KEYS) $stdout.flush end begin # readmultiline with confirm block that *always* returns true: # => default (plain) ENTER triggers finish/submit of the (multi-line) buffer # SHIFT+ENTER (matched seq) triggers :key_newline (insert \n, stay in edit mode) # Reline handles multi-line pastes by splitting on \n in the buffer. @line_buffer = Reline.readmultiline(prompt, true) { |_buffer| true } || '' ensure if tty $stdout.write(DISABLE_EXTENDED_KEYS) $stdout.flush end end @line_buffer end |
#reline_config ⇒ Object
Reline ≤ 0.5.x exposed a top-level Reline.config delegator.
Reline ≥ 0.6.x removed it; the Config object now lives only on
the (private) singleton Reline.core. Probe in order of
preference so the same code works across both.
88 89 90 91 92 93 |
# File 'lib/pwn/plugins/repl.rb', line 88 def reline_config return Reline.config if Reline.respond_to?(:config) return Reline.core.config if Reline.respond_to?(:core) Reline.send(:core).config end |
#tty? ⇒ Boolean
Compatibility with Pry input expectations
212 213 214 |
# File 'lib/pwn/plugins/repl.rb', line 212 def tty? true end |
#winsize ⇒ Object
216 217 218 |
# File 'lib/pwn/plugins/repl.rb', line 216 def winsize [TTY::Screen.rows || 24, TTY::Screen.columns || 80] end |