Skip to content

Commit f3734bd

Browse files
committed
TUI: explicit Option/Meta+Backspace word-delete + openhack --showkey
Bind word-delete (erase previous word) explicitly and eagerly to the escape-prefixed backspace/delete encodings — (escape, backspace) [ESC+DEL \x1b\x7f and ESC+BS, both parse to (escape, c-h)], (escape, delete) [the Option+forward-delete CSI], and c-w — so it's deterministic across terminals and can't be pre-empted by the lone-escape handlers. Verified in a real pseudo-terminal: "hello world foo" + each sequence -> "hello world "; a plain backspace still deletes one char. Adds `openhack --showkey`: a raw-key diagnostic that prints the exact byte sequence a terminal sends for each keypress (e.g. Option+Backspace), so a nonstandard encoding can be identified and bound precisely. Claude-Session: https://claude.ai/code/session_016PMahuFwyaHhR5aUeeC1TM
1 parent 4343cf4 commit f3734bd

2 files changed

Lines changed: 65 additions & 0 deletions

File tree

‎openhack/__main__.py‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
openhack --providers List LLM providers you can connect
1414
openhack --login Log in to your OpenHack account
1515
openhack --setup Run the setup wizard
16+
openhack --showkey Print the raw bytes your terminal sends (key diagnostic)
1617
openhack --help Show usage
1718
"""
1819

@@ -264,6 +265,52 @@ def _cmd_classify():
264265
print(f" Run 'openhack --resume {sid}' to scan\n")
265266

266267

268+
def _cmd_showkey():
269+
"""Print the raw byte sequence your terminal sends for each keypress.
270+
271+
Diagnostic for key-binding issues (e.g. Option+Backspace): run it, press
272+
the key, and it prints the exact bytes so a binding can target them.
273+
"""
274+
import os
275+
import select
276+
277+
try:
278+
import termios
279+
import tty
280+
except ImportError:
281+
print("showkey needs a POSIX terminal.")
282+
return
283+
if not sys.stdin.isatty():
284+
print("Run this in a real terminal (stdin is not a TTY).")
285+
return
286+
287+
fd = sys.stdin.fileno()
288+
old = termios.tcgetattr(fd)
289+
print("Press keys to see what your terminal sends (Ctrl-C to quit).", flush=True)
290+
print("Try: Option+Backspace, Ctrl-W, Option+Left.\n", flush=True)
291+
try:
292+
tty.setraw(fd)
293+
while True:
294+
if not select.select([fd], [], [], None)[0]:
295+
continue
296+
buf = os.read(fd, 1)
297+
# Drain the rest of a multi-byte escape sequence.
298+
while select.select([fd], [], [], 0.02)[0]:
299+
buf += os.read(fd, 1)
300+
if buf in (b"\x03", b"\x04"): # Ctrl-C / Ctrl-D
301+
break
302+
termios.tcsetattr(fd, termios.TCSADRAIN, old) # restore to print
303+
hexs = " ".join(f"\\x{c:02x}" for c in buf)
304+
pretty = buf.decode("latin-1").replace("\x1b", "ESC")
305+
print(f" {hexs:<20} ({pretty!r})", flush=True)
306+
tty.setraw(fd)
307+
except KeyboardInterrupt:
308+
pass
309+
finally:
310+
termios.tcsetattr(fd, termios.TCSADRAIN, old)
311+
print(flush=True)
312+
313+
267314
def _cmd_login():
268315
"""Run the device login flow."""
269316
from openhack.setup import run_first_time_setup
@@ -288,6 +335,7 @@ def _cmd_setup():
288335
"classify": _cmd_classify,
289336
"login": _cmd_login,
290337
"setup": _cmd_setup,
338+
"showkey": _cmd_showkey,
291339
}
292340

293341

‎openhack/tui.py‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1359,6 +1359,23 @@ def _select_word_right(event):
13591359
buf.start_selection()
13601360
buf.cursor_position += pos
13611361

1362+
# Option/Meta + Backspace — erase the word before the cursor (the
1363+
# standard word-delete every shell/editor does). macOS sends ESC + DEL
1364+
# (\x1b\x7f) which prompt_toolkit parses as (escape, c-h); the ESC+BS and
1365+
# Option+forward-delete variants land on (escape, delete). We bind them
1366+
# explicitly and eagerly so the behavior is deterministic and can't be
1367+
# pre-empted by the lone-escape handlers.
1368+
@kb.add("escape", "backspace", eager=True, filter=~modal_open)
1369+
@kb.add("escape", "delete", eager=True, filter=~modal_open)
1370+
@kb.add("c-w", eager=True, filter=~modal_open)
1371+
def _delete_word_before(event):
1372+
buf = event.current_buffer
1373+
pos = buf.document.find_previous_word_beginning()
1374+
if pos:
1375+
buf.delete_before_cursor(-pos)
1376+
elif buf.document.text_before_cursor:
1377+
buf.delete_before_cursor(len(buf.document.text_before_cursor))
1378+
13621379
# Tab navigation — only in scanning/viewing modes, and only when the
13631380
# input is empty so they don't conflict with typing.
13641381
def _in_tabs() -> bool:

0 commit comments

Comments
 (0)