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. Plain ENTER submits the full (possibly multi-line) buffer; a newline is inserted (keep editing) by ANY of:
- SHIFT+ENTER on capable terminals (kitty, wezterm, foot,
alacritty, xterm, Konsole, iTerm2, Windows
Terminal; and Terminator via `pwn setup --terminal`)
- ALT+ENTER terminal-agnostic fallback — every emulator,
including bare VTE, sends `\e\r` for this.
- trailing `\` bash/zsh/irb/psql-style continuation: end a line
with backslash + ENTER to keep composing. The
backslashes are stripped before submit.
Multi-line pastes work (Reline holds \n in the buffer; ENTER submits).
See documentation/pwn-REPL.md § Multi-line input for the terminal
support matrix and pwn setup --terminal for the opt-in VTE fix.
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 ALT+ENTER — universal fallback (all emulators, incl. VTE) [27, 10], # \e\n ALT+ENTER (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. -
#ensure_vte_shift_enter ⇒ Object
VTE-based emulators (Terminator, GNOME Terminal, Tilix, xfce4-terminal, Guake, Ptyxis, MATE Terminal, ...) do not implement xterm modifyOtherKeys (CSI >4;Nm) nor the kitty keyboard protocol (CSI >1u) — see GNOME/vte issues #2601 and #2607.
-
#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.
85 86 87 88 89 90 91 |
# File 'lib/pwn/plugins/repl.rb', line 85 def initialize(pry_instance) @line_buffer = '' pry_instance.config.pwn_ai_original_input = Pry.input ensure_tmux_extended_keys ensure_vte_shift_enter install_shift_enter_bindings end |
Instance Attribute Details
#line_buffer ⇒ Object (readonly)
Returns the value of attribute line_buffer.
31 32 33 |
# File 'lib/pwn/plugins/repl.rb', line 31 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).
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 158 159 160 161 162 163 164 165 166 |
# File 'lib/pwn/plugins/repl.rb', line 119 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 |
#ensure_vte_shift_enter ⇒ Object
VTE-based emulators (Terminator, GNOME Terminal, Tilix, xfce4-terminal, Guake, Ptyxis, MATE Terminal, ...) do not implement xterm modifyOtherKeys (CSI >4;Nm) nor the kitty keyboard protocol (CSI >1u) — see GNOME/vte issues #2601 and #2607. The ENABLE_EXTENDED_KEYS request we emit (and that tmux emits to the outer terminal via Eneks) is silently ignored, so a physical Shift+Enter reaches us as an indistinguishable plain 0x0D. No tmux/Reline configuration can fix that — the modifier was lost at the outer terminal.
This method therefore ONLY detects and hints. It never mutates the
user's host. Terminal-agnostic fallbacks (Alt+Enter, trailing \)
already work; for real Shift+Enter under Terminator the user can
opt in with pwn setup --terminal, which installs
third_party/terminator/pwn_shift_enter.py into
~/.config/terminator/plugins/ after asking permission.
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 |
# File 'lib/pwn/plugins/repl.rb', line 184 def ensure_vte_shift_enter return if self.class.instance_variable_get(:@vte_shift_enter_checked) self.class.instance_variable_set(:@vte_shift_enter_checked, true) vte_ver = ENV['VTE_VERSION'].to_s return if vte_ver.empty? in_terminator = !ENV['TERMINATOR_UUID'].to_s.empty? || !ENV['TERMINATOR_DBUS_NAME'].to_s.empty? # If the plugin is already installed & enabled, stay silent. if in_terminator cfg = File.join(Dir.home, '.config', 'terminator', 'config') return if File.exist?(cfg) && File.read(cfg).include?('PWNShiftEnter') end host = if in_terminator then 'Terminator' elsif ENV['GNOME_TERMINAL_SCREEN'] || ENV['GNOME_TERMINAL_SERVICE'] then 'GNOME Terminal' elsif ENV['TILIX_ID'] then 'Tilix' else "a VTE-#{vte_ver} terminal" end warn "[pwn] #{host} (libvte) can't distinguish SHIFT+ENTER from ENTER — the modifier is dropped at the emulator." warn '[pwn] Multi-line input still works: use ALT+ENTER, or end the line with `\` then ENTER.' if in_terminator warn '[pwn] For real SHIFT+ENTER support here, run: `pwn setup --terminal` (installs a Terminator plugin — opt-in, one-time).' else warn '[pwn] For native SHIFT+ENTER, use kitty / wezterm / foot / alacritty / xterm / Konsole / iTerm2 / Terminator.' end rescue StandardError => e warn "[pwn] ensure_vte_shift_enter: #{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.
230 231 232 233 234 235 236 237 238 239 240 |
# File 'lib/pwn/plugins/repl.rb', line 230 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
242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 |
# File 'lib/pwn/plugins/repl.rb', line 242 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 # Plain ENTER submits UNLESS the last non-whitespace char on the # last line is `\` (bash/irb/psql-style continuation) — that is # the terminal-agnostic fallback for emulators that can't send a # distinct SHIFT+ENTER (all VTE hosts). SHIFT+ENTER / ALT+ENTER # (matched via SHIFT_ENTER_SEQS) trigger :key_newline directly. # Reline handles multi-line pastes by splitting on \n in-buffer. @line_buffer = Reline.readmultiline(prompt, true) do |buffer| !buffer.split("\n", -1).last.to_s.rstrip.end_with?('\\') end || '' # Strip the continuation markers before handing off to the caller. @line_buffer = @line_buffer.gsub(/\\[ \t]*\n/, "\n") 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.
97 98 99 100 101 102 |
# File 'lib/pwn/plugins/repl.rb', line 97 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
274 275 276 |
# File 'lib/pwn/plugins/repl.rb', line 274 def tty? true end |
#winsize ⇒ Object
278 279 280 |
# File 'lib/pwn/plugins/repl.rb', line 278 def winsize [TTY::Screen.rows || 24, TTY::Screen.columns || 80] end |