Skip to content

Commit 59fe401

Browse files
committed
fix(terminal): revert macOptionIsMeta, scope Option+Arrow word-jump instead
macOptionIsMeta:true (previous commit) would have turned every Option-modified keystroke into an ESC-prefixed byte, breaking the Option-key dead-key accent composition (ñ, á, ü, ...) that xtermDeadKeyAddon.ts exists to support on Spanish/Portuguese/US-Intl layouts. Revert it and instead hand-emit ESC b / ESC f only for plain Option+Left/Option+Right, which readline's default emacs keymap already binds to backward-word/forward-word — every other Option chord is left alone. Also adds a Rust test that runs the real ZDOTDIR-indirection bootstrap through an actual zsh binary and asserts a stand-in user .zshrc is sourced, giving real macOS-runner verification of that code path (previously only reasoned about statically) since local dev here is Windows.
1 parent 7d7a556 commit 59fe401

6 files changed

Lines changed: 68 additions & 10 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "afkode",
33
"private": true,
4-
"version": "0.8.16",
4+
"version": "0.8.17",
55
"type": "module",
66
"scripts": {
77
"dev": "vite",

src-tauri/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "afkode"
3-
version = "0.8.16"
3+
version = "0.8.17"
44
description = "In-game overlay to supervise AI coding agents while you play"
55
authors = ["Omar Hernandez"]
66
license = "MIT"

src-tauri/src/lib.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,51 @@ mod tests {
523523
assert_eq!(sh_squote("it's"), r"'it'\''s'");
524524
}
525525

526+
// Runs the real ZDOTDIR-indirection bootstrap through an actual zsh
527+
// binary (present on the macOS CI runner, where oh-my-zsh sourcing was
528+
// otherwise only checkable by hand) and asserts a stand-in for the
529+
// user's real .zshrc (an oh-my-zsh install sources exactly the same
530+
// way) is reached.
531+
#[cfg(not(target_os = "windows"))]
532+
#[test]
533+
fn zsh_zdotdir_bootstrap_sources_user_zshrc() {
534+
if std::process::Command::new("zsh")
535+
.arg("--version")
536+
.output()
537+
.is_err()
538+
{
539+
eprintln!("zsh not found on this machine, skipping");
540+
return;
541+
}
542+
let tmp = std::env::temp_dir().join(format!("afk-zsh-test-{}", std::process::id()));
543+
let user_home = tmp.join("home");
544+
std::fs::create_dir_all(&user_home).unwrap();
545+
std::fs::write(
546+
user_home.join(".zshrc"),
547+
"export AFK_TEST_MARKER=from-user-zshrc\n",
548+
)
549+
.unwrap();
550+
551+
let integration_dir = write_shell_integration().expect("write_shell_integration failed");
552+
553+
let output = std::process::Command::new("zsh")
554+
.env("ZDOTDIR", integration_dir.join("zdotdir"))
555+
.env("AFKODE_USER_ZDOTDIR", &user_home)
556+
.args(["-i", "-c", "echo $AFK_TEST_MARKER"])
557+
.output()
558+
.expect("failed to spawn zsh");
559+
560+
let stdout = String::from_utf8_lossy(&output.stdout);
561+
assert!(
562+
stdout.contains("from-user-zshrc"),
563+
"expected the ZDOTDIR bootstrap to source the user's real .zshrc; \
564+
stdout={stdout:?} stderr={:?}",
565+
String::from_utf8_lossy(&output.stderr)
566+
);
567+
568+
let _ = std::fs::remove_dir_all(&tmp);
569+
}
570+
526571
#[cfg(target_os = "windows")]
527572
#[test]
528573
fn ps_squote_escapes_single_quotes() {

src-tauri/tauri.conf.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://schema.tauri.app/config/2",
33
"productName": "AFKode",
4-
"version": "0.8.16",
4+
"version": "0.8.17",
55
"identifier": "app.afkode.overlay",
66
"build": {
77
"beforeDevCommand": "npm run dev",

src/main.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1342,12 +1342,6 @@ async function newSession(
13421342
minimumContrastRatio: 1,
13431343
cursorBlink: true,
13441344
cursorStyle: "bar",
1345-
// Without this, xterm.js treats Option as a dead-key/composition
1346-
// modifier on macOS instead of sending ESC-prefixed bytes, so
1347-
// Option+Left/Right never reach the shell as the ESC b / ESC f
1348-
// sequences readline (and Claude Code's own Option-based input)
1349-
// expect for word-wise cursor movement.
1350-
macOptionIsMeta: true,
13511345
// Memory: 2k lines is plenty for supervision; agents redraw their TUI.
13521346
scrollback: 2000,
13531347
theme: themeDef.term,
@@ -1474,6 +1468,25 @@ async function newSession(
14741468
term.attachCustomKeyEventHandler((ev) => {
14751469
if (deadKey.handle(ev)) return false;
14761470
if (ev.type !== "keydown") return true;
1471+
// Option+Left/Right word-jump. macOptionIsMeta would fix this at the
1472+
// xterm.js level, but it turns *every* Option-modified key into an
1473+
// ESC-prefixed byte — breaking Option-key dead-key accent composition
1474+
// (ñ, á, ü, ...) that xtermDeadKeyAddon exists to support. So instead
1475+
// we hand-emit the ESC b / ESC f sequences readline's default emacs
1476+
// keymap already binds to backward-word/forward-word, scoped to just
1477+
// these two keys, leaving every other Option chord untouched.
1478+
if (
1479+
isMac() &&
1480+
ev.altKey &&
1481+
!ev.metaKey &&
1482+
!ev.ctrlKey &&
1483+
!ev.shiftKey &&
1484+
(ev.code === "ArrowLeft" || ev.code === "ArrowRight")
1485+
) {
1486+
ev.preventDefault();
1487+
invoke("write_pty", { id, data: ev.code === "ArrowLeft" ? "\x1bb" : "\x1bf" }).catch(() => {});
1488+
return false;
1489+
}
14771490
// AI command search (F3) — shell tabs only, so agent TUIs never see it.
14781491
// '#' only fires at a verifiably idle, empty prompt (OSC 133 state);
14791492
// Ctrl+Space works regardless, as the explicit opt-in gesture.

0 commit comments

Comments
 (0)