diff --git a/migrations/1780057136.sh b/migrations/1780057136.sh new file mode 100644 index 00000000..1e6a8c10 --- /dev/null +++ b/migrations/1780057136.sh @@ -0,0 +1,172 @@ +echo "Make Shift+Enter distinguishable for terminals and Codex" + +alacritty_config="$HOME/.config/alacritty/alacritty.toml" +foot_config="$HOME/.config/foot/foot.ini" +ghostty_config="$HOME/.config/ghostty/config" +kitty_config="$HOME/.config/kitty/kitty.conf" + +ensure_alacritty_shift_return() { + local config="$1" + + [[ -f $config ]] || return 0 + + if grep -q 'key = "Return", mods = "Shift", chars = "\\u001B\\r"' "$config"; then + sed -i 's/{ key = "Return", mods = "Shift", chars = "\\u001B\\r" }/{ key = "Return", mods = "Shift", chars = "\\u001B[13;2u" }/' "$config" + elif ! grep -q 'key = "Return", mods = "Shift", chars = "\\u001B\[13;2u"' "$config"; then + if grep -qxF '{ key = "Return", mods = "Alt|Shift", chars = "\u001B[13;4u" }' "$config"; then + sed -i '/^{ key = "Return", mods = "Alt|Shift", chars = "\\u001B\[13;4u" }$/i # Send Shift+Return as CSI-u so TUIs can distinguish it from Return without treating it as Alt+Return.\n{ key = "Return", mods = "Shift", chars = "\\u001B[13;2u" },' "$config" + elif grep -qxF '{ key = "Insert", mods = "Control", action = "Copy" },' "$config"; then + sed -i '/^{ key = "Insert", mods = "Control", action = "Copy" },$/a # Send Shift+Return as CSI-u so TUIs can distinguish it from Return without treating it as Alt+Return.\n{ key = "Return", mods = "Shift", chars = "\\u001B[13;2u" },' "$config" + else + printf '\n# Send Shift+Return as CSI-u so TUIs can distinguish it from Return without treating it as Alt+Return.\n{ key = "Return", mods = "Shift", chars = "\\u001B[13;2u" }\n' >>"$config" + fi + fi +} + +ensure_ghostty_binding() { + local config="$1" + local key="$2" + local binding="$3" + local comment="$4" + local key_regex + + [[ -f $config ]] || return 0 + + key_regex=$(printf '%s\n' "$key" | sed 's/[][\\.^$*+?{}|()\/]/\\&/g') + + if grep -Eq "^keybind = $key_regex=.*13;[24]u" "$config"; then + sed -i "s/^keybind = $key_regex=.*/keybind = $key=$binding/" "$config" + elif ! grep -qF "keybind = $key=" "$config"; then + if grep -qxF 'keybind = control+insert=copy_to_clipboard' "$config"; then + sed -i "/^keybind = control+insert=copy_to_clipboard$/a $comment\nkeybind = $key=$binding" "$config" + else + printf '\n%s\nkeybind = %s=%s\n' "$comment" "$key" "$binding" >>"$config" + fi + fi +} + +ensure_kitty_binding() { + local config="$1" + local key="$2" + local binding="$3" + local comment="$4" + local key_regex + local tmp + + [[ -f $config ]] || return 0 + + key_regex=$(printf '%s\n' "$key" | sed 's/[][\\.^$*+?{}|()\/]/\\&/g') + + if grep -Eq "^map[[:space:]]+$key_regex[[:space:]].*13;[24]u" "$config"; then + tmp=$(mktemp) + KEY="$key" BINDING="$binding" awk ' + BEGIN { + key = ENVIRON["KEY"] + binding = ENVIRON["BINDING"] + } + { + split($0, parts, /[[:space:]]+/) + if (parts[1] == "map" && parts[2] == key && $0 ~ /13;[24]u/) { + $0 = "map " key " " binding + } + print + } + ' "$config" >"$tmp" && mv "$tmp" "$config" + elif ! grep -Eq "^map[[:space:]]+$key_regex[[:space:]]" "$config"; then + if grep -qxF 'map shift+insert paste_from_clipboard' "$config"; then + tmp=$(mktemp) + KEY="$key" BINDING="$binding" COMMENT="$comment" awk ' + BEGIN { + key = ENVIRON["KEY"] + binding = ENVIRON["BINDING"] + comment = ENVIRON["COMMENT"] + } + { print } + !inserted && $0 == "map shift+insert paste_from_clipboard" { + print comment + print "map " key " " binding + inserted = 1 + } + ' "$config" >"$tmp" && mv "$tmp" "$config" + else + printf '\n%s\nmap %s %s\n' "$comment" "$key" "$binding" >>"$config" + fi + fi +} + +ensure_foot_text_binding() { + local config="$1" + local sequence="$2" + local binding="$3" + local comment="$4" + local tmp + + [[ -f $config ]] || return 0 + + if grep -qF "$sequence=" "$config"; then + tmp=$(mktemp) + SEQUENCE="$sequence" BINDING="$binding" awk ' + BEGIN { + sequence = ENVIRON["SEQUENCE"] + binding = ENVIRON["BINDING"] + } + index($0, sequence "=") == 1 { $0 = sequence "=" binding } + { print } + ' "$config" >"$tmp" && mv "$tmp" "$config" + elif grep -qxF '[text-bindings]' "$config"; then + tmp=$(mktemp) + SEQUENCE="$sequence" BINDING="$binding" COMMENT="$comment" awk ' + BEGIN { + sequence = ENVIRON["SEQUENCE"] + binding = ENVIRON["BINDING"] + comment = ENVIRON["COMMENT"] + } + { print } + !inserted && $0 == "[text-bindings]" { + print comment + print sequence "=" binding + inserted = 1 + } + ' "$config" >"$tmp" && mv "$tmp" "$config" + else + printf '\n[text-bindings]\n%s\n%s=%s\n' "$comment" "$sequence" "$binding" >>"$config" + fi +} + +ensure_alacritty_shift_return "$alacritty_config" + +ensure_ghostty_binding \ + "$ghostty_config" \ + "shift+enter" \ + "csi:13;2u" \ + "# Send Shift+Enter as CSI-u so TUIs can distinguish it from Enter." + +ensure_ghostty_binding \ + "$ghostty_config" \ + "alt+shift+enter" \ + "csi:13;4u" \ + "# Legacy encoding sends Alt+Shift+Enter the same as Alt+Enter; send CSI-u so tmux can match M-S-Enter." + +ensure_kitty_binding \ + "$kitty_config" \ + "shift+enter" \ + "send_text all \\e[13;2u" \ + "# Send Shift+Enter as CSI-u so TUIs can distinguish it from Enter." + +ensure_kitty_binding \ + "$kitty_config" \ + "alt+shift+enter" \ + "send_text all \\e[13;4u" \ + "# Kitty legacy encoding sends Alt+Shift+Enter the same as Alt+Enter; send CSI-u so tmux can match M-S-Enter." + +ensure_foot_text_binding \ + "$foot_config" \ + "\\x1b[13;2u" \ + "Shift+Return" \ + "# Send Shift+Return as CSI-u so TUIs can distinguish it from Return." + +ensure_foot_text_binding \ + "$foot_config" \ + "\\x1b[13;4u" \ + "Mod1+Shift+Return" \ + "# Send Alt+Shift+Return as CSI-u so tmux can match M-S-Enter." diff --git a/test/shell.d/terminal-keybinding-migration-test.sh b/test/shell.d/terminal-keybinding-migration-test.sh new file mode 100644 index 00000000..04a4c373 --- /dev/null +++ b/test/shell.d/terminal-keybinding-migration-test.sh @@ -0,0 +1,59 @@ +#!/bin/bash + +source "$(dirname "${BASH_SOURCE[0]}")/base-test.sh" + +migration=$(grep -rl 'Make Shift+Enter distinguishable for terminals and Codex' "$ROOT/migrations" | head -n 1 || true) +[[ -n $migration ]] || fail "terminal Shift+Enter migration exists" + +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +test_home="$TMPDIR/home" +mkdir -p \ + "$test_home/.config/alacritty" \ + "$test_home/.config/foot" \ + "$test_home/.config/ghostty" \ + "$test_home/.config/kitty" + +cp "$ROOT/config/alacritty/alacritty.toml" "$test_home/.config/alacritty/alacritty.toml" +sed -i 's/\\u001B\[13;2u/\\u001B\\r/' "$test_home/.config/alacritty/alacritty.toml" + +cp "$ROOT/config/kitty/kitty.conf" "$test_home/.config/kitty/kitty.conf" +sed -i '/shift+enter/d' "$test_home/.config/kitty/kitty.conf" + +cp "$ROOT/config/ghostty/config" "$test_home/.config/ghostty/config" +sed -i '/shift+enter/d' "$test_home/.config/ghostty/config" + +cp "$ROOT/config/foot/foot.ini" "$test_home/.config/foot/foot.ini" +sed -i '/text-bindings/,$d' "$test_home/.config/foot/foot.ini" + +HOME="$test_home" bash "$migration" >/dev/null + +grep -qxF '{ key = "Return", mods = "Shift", chars = "\u001B[13;2u" },' "$test_home/.config/alacritty/alacritty.toml" || + fail "migration updates Alacritty Shift+Return to CSI-u" +pass "migration updates Alacritty Shift+Return to CSI-u" + +grep -qxF 'map shift+enter send_text all \e[13;2u' "$test_home/.config/kitty/kitty.conf" || + fail "migration adds Kitty Shift+Enter CSI-u binding" +grep -qxF 'map alt+shift+enter send_text all \e[13;4u' "$test_home/.config/kitty/kitty.conf" || + fail "migration adds Kitty Alt+Shift+Enter CSI-u binding" +pass "migration adds Kitty CSI-u bindings" + +grep -qxF 'keybind = shift+enter=csi:13;2u' "$test_home/.config/ghostty/config" || + fail "migration adds Ghostty Shift+Enter CSI-u binding" +grep -qxF 'keybind = alt+shift+enter=csi:13;4u' "$test_home/.config/ghostty/config" || + fail "migration adds Ghostty Alt+Shift+Enter CSI-u binding" +pass "migration adds Ghostty CSI-u bindings" + +grep -qxF '\x1b[13;2u=Shift+Return' "$test_home/.config/foot/foot.ini" || + fail "migration adds Foot Shift+Return CSI-u binding" +grep -qxF '\x1b[13;4u=Mod1+Shift+Return' "$test_home/.config/foot/foot.ini" || + fail "migration adds Foot Alt+Shift+Return CSI-u binding" +pass "migration adds Foot CSI-u bindings" + +before=$(find "$test_home/.config" -type f -print0 | sort -z | xargs -0 sha256sum) +HOME="$test_home" bash "$migration" >/dev/null +after=$(find "$test_home/.config" -type f -print0 | sort -z | xargs -0 sha256sum) + +[[ $before == "$after" ]] || fail "terminal Shift+Enter migration is idempotent" +pass "terminal Shift+Enter migration is idempotent"