Files
Kigi-CLI/crates/codegen/kigi-pager-render/src/terminal/keyboard.rs
T
ZacharyZhang-NY 6f31415ed6 §9 acceptance: grep-zero sweep — every internal x.ai/grok identifier renamed
The PRD's first acceptance gate now holds: grep -RinE '\bx\.ai\b|grok'
crates/ --include='*.rs' → 0 matches (exempt: NOTICE and third-party
license archives, README provenance, and the required 'Based on Grok
Build Open Source' attribution, now sourced from version_attribution.txt).

Wire-visible renames (both sides in this repo, changed in lockstep):
- Auth method id 'grok.com' → 'kimi-code' (AuthMethodKind::KimiCode).
- Every x.ai/* and _x.ai/* ACP ext method and meta key → kigi/* /
  _kigi/* (~200 names; grokShell → kigiShell). Session-file replay keeps
  a read-side alias for the legacy '_x.ai/session/update' method so
  existing updates.jsonl histories load; writes emit only the new name
  (both directions test-pinned).
- Agent types grok-build* → kigi* with a documented legacy-prefix alias
  at resolution time so persisted sessions keep resolving.
- ToolNamespace/BuiltinAgentName GrokBuild* → Kigi* (wire snake_case
  kigi/kigi_concise/kigi_hashline; schema regenerated); grok_build
  implementation dirs renamed to kigi*.
- x-grok-* headers → x-kigi-*, __GROK_* sentinels → __KIGI_*, themes
  grokday/groknight → kigiday/kiginight (old persisted values fall back
  to the default theme), web_fetch allowlist xAI hosts → kimi.com +
  moonshot platforms, changelog CDN → this repo, grok-build changelog
  archives deleted.
- BYOK default endpoint removed: [endpoints] api_base_url is now truly
  optional with NO default — consumers fail fast with the flag name when
  unset (no silent x.ai egress). Mock harnesses inject it explicitly.
- System-prompt identity fixed: 'released by xAI' → 'an unofficial
  community CLI for Kimi' (template + regenerated encrypted form).

Also repaired pre-existing grok-era test debt found by the sweep: the
stale trace_classify default-model pin, the grok-pager UA label test,
pty-harness stale-binary reuse and non-hermetic moonshot routing (a PTY
test could previously reach the real api.moonshot.cn), and the outdated
oauth fixture scope key.

Gates: §9 grep 0; fmt clean; workspace check/clippy 0/0 (-D warnings);
FULL cargo test --workspace: 234 suites, 21,961 passed, 0 failed;
deny advisories ok.
2026-07-18 02:48:46 -04:00

216 lines
8.2 KiB
Rust

//! Per-terminal keyboard input capabilities.
//!
//! Classifies keyboard delivery semantics so input-handling code can consume one struct
//! instead of branching on brand. The classification depends on the
//! current `HostOs`, queried internally — today only macOS rows are
//! populated. Extend [`KeyboardCapabilities`] with new fields (paste
//! protocol, focus reporting, custom escapes) instead of adding more
//! `match self.brand` sites scattered through the pager.
use super::TerminalName;
use crate::host::HostOs;
/// What happens to a single modifier (Cmd, Opt, etc.) on its way from
/// the keyboard to the program.
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, strum::Display)]
#[strum(serialize_all = "snake_case")]
#[non_exhaustive]
pub enum ModifierFate {
/// Terminal delivers the modifier in the `KeyEvent` (KKP) or as a
/// readline-equivalent byte sequence the textarea already handles
/// (`^U`, `ESC ^?`).
Native,
/// Terminal drops the modifier; the OS-level rescue can recover it
/// (CoreGraphics on macOS).
Dropped,
/// Chord captured before reaching the PTY (Apple Terminal Cmd+Bsp).
/// No event arrives; not even an OS rescue helps.
Unrecoverable,
/// Behavior unclassified — treated as no-rescue to avoid false
/// positives on unknown brands.
#[default]
Unknown,
}
impl ModifierFate {
pub fn benefits_from_rescue(self) -> bool {
matches!(self, Self::Dropped)
}
}
/// How a terminal delivers Cmd/Opt-modified Backspace/Delete.
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
#[non_exhaustive]
pub struct ModifierDelivery {
pub cmd: ModifierFate,
pub opt: ModifierFate,
}
impl ModifierDelivery {
/// Construct a delivery from explicit fates. `#[non_exhaustive]` blocks
/// struct-literal construction from other crates, so downstream test
/// builds use this constructor.
#[cfg(any(test, feature = "test-support"))]
pub fn new_for_test(cmd: ModifierFate, opt: ModifierFate) -> Self {
Self { cmd, opt }
}
pub fn benefits_from_rescue(self) -> bool {
self.cmd.benefits_from_rescue() || self.opt.benefits_from_rescue()
}
pub fn label(self) -> String {
format!("cmd={}, opt={}", self.cmd, self.opt)
}
}
/// Per-terminal keyboard capabilities. Extend with new fields as more
/// per-terminal input behaviors get classified.
#[derive(Debug, Clone, Copy, Default)]
#[non_exhaustive]
pub struct KeyboardCapabilities {
pub modifier_delivery: ModifierDelivery,
/// Fate of Shift/Opt/Cmd when modifying `Enter`. Apple Terminal
/// drops these and we recover them via the same OS poll used for
/// Backspace/Delete.
pub enter_modifier: ModifierFate,
}
impl KeyboardCapabilities {
pub fn enter_needs_rescue(&self) -> bool {
matches!(self.enter_modifier, ModifierFate::Dropped)
}
}
/// Classify keyboard capabilities for a given `(brand, os, display_server)`.
///
/// Today the table is populated only for macOS; other OSes return the
/// default (all-`Unknown`). When a Linux/Windows probe lands, add a
/// per-OS arm here rather than forking the function.
pub fn keyboard_capabilities(brand: TerminalName) -> KeyboardCapabilities {
match HostOs::current() {
HostOs::Macos => macos_capabilities(brand),
HostOs::Linux | HostOs::Windows | HostOs::Other => KeyboardCapabilities::default(),
}
}
fn macos_capabilities(brand: TerminalName) -> KeyboardCapabilities {
use ModifierFate::*;
let (cmd, opt, enter) = match brand {
TerminalName::Ghostty | TerminalName::Kitty | TerminalName::Foot => {
(Native, Native, Native)
}
// iTerm2/VS Code translate Cmd+Bsp → ^U and Opt+Bsp → ESC ^?,
// both of which the textarea already handles natively. VS Code-family
// embeds and Zed inherit the same keymap behavior as VS Code
// capabilities at runtime (no TERM_FEATURES, XTVERSION leaks).
// (including the Cmd+Bsp → ^U translation).
TerminalName::Iterm2
| TerminalName::VsCode
| TerminalName::Cursor
| TerminalName::Windsurf
| TerminalName::Zed => (Native, Native, Native),
TerminalName::WezTerm => (Dropped, Native, Native),
// Alacritty's macOS keymap binds Cmd+Bsp → ^U (native readline);
// Opt+Bsp is a bare ^? without `option_as_alt` set.
TerminalName::Alacritty | TerminalName::Rio => (Native, Dropped, Native),
TerminalName::WarpTerminal => (Dropped, Dropped, Native),
// Apple Terminal: Cmd+Bsp captured by the window manager.
// Opt+Bsp and modified Enter are dropped; CG can rescue both.
TerminalName::AppleTerminal => (Unrecoverable, Dropped, Dropped),
TerminalName::KigiDesktop => (Unknown, Unknown, Unknown),
// VTE-based terminals (incl. Terminator) on macOS are unusual;
// classify when we have evidence rather than guessing.
TerminalName::Vte | TerminalName::Terminator => (Unknown, Unknown, Unknown),
// JetBrains JediTerm: no KKP, no CG rescue. No way to probe
// Mouse reporting has known SGR bugs in Classic engine (IJPL-232482);
// Mouse reporting has known SGR bugs in Classic engine (IJPL-232482);
// Reworked 2025 engine is better but indistinguishable via env vars.
TerminalName::JetBrains => (Unknown, Unknown, Unknown),
TerminalName::WindowsTerminal | TerminalName::Otty | TerminalName::Unknown => {
(Unknown, Unknown, Unknown)
}
};
KeyboardCapabilities {
modifier_delivery: ModifierDelivery { cmd, opt },
enter_modifier: enter,
}
}
// Tests verify the macOS classification table; they only mean
// something on a macOS host. Cross-OS verification will need a test
// harness that overrides `HostOs::current()`; see the module doc.
#[cfg(all(test, target_os = "macos"))]
mod tests {
use super::*;
#[test]
fn ghostty_and_kitty_native_no_rescue() {
for brand in [TerminalName::Ghostty, TerminalName::Kitty] {
let c = keyboard_capabilities(brand);
assert_eq!(c.modifier_delivery.cmd, ModifierFate::Native);
assert_eq!(c.modifier_delivery.opt, ModifierFate::Native);
assert!(!c.modifier_delivery.benefits_from_rescue());
}
}
#[test]
fn iterm_and_vscode_readline_no_rescue() {
for brand in [TerminalName::Iterm2, TerminalName::VsCode] {
assert!(
!keyboard_capabilities(brand)
.modifier_delivery
.benefits_from_rescue()
);
}
}
#[test]
fn wezterm_drops_cmd_keeps_opt() {
let c = keyboard_capabilities(TerminalName::WezTerm);
assert_eq!(c.modifier_delivery.cmd, ModifierFate::Dropped);
assert_eq!(c.modifier_delivery.opt, ModifierFate::Native);
assert!(c.modifier_delivery.benefits_from_rescue());
}
#[test]
fn alacritty_keeps_cmd_drops_opt() {
let c = keyboard_capabilities(TerminalName::Alacritty);
assert_eq!(c.modifier_delivery.cmd, ModifierFate::Native);
assert_eq!(c.modifier_delivery.opt, ModifierFate::Dropped);
assert!(c.modifier_delivery.benefits_from_rescue());
}
#[test]
fn rio_keeps_cmd_drops_opt() {
let c = keyboard_capabilities(TerminalName::Rio);
assert_eq!(c.modifier_delivery.cmd, ModifierFate::Native);
assert_eq!(c.modifier_delivery.opt, ModifierFate::Dropped);
assert!(c.modifier_delivery.benefits_from_rescue());
}
#[test]
fn apple_terminal_cmd_unrecoverable_opt_dropped() {
let c = keyboard_capabilities(TerminalName::AppleTerminal);
assert_eq!(c.modifier_delivery.cmd, ModifierFate::Unrecoverable);
assert_eq!(c.modifier_delivery.opt, ModifierFate::Dropped);
assert!(c.modifier_delivery.benefits_from_rescue());
assert!(c.enter_needs_rescue());
}
#[test]
fn unknown_brands_skip_rescue() {
for brand in [
TerminalName::Unknown,
TerminalName::KigiDesktop,
TerminalName::Vte,
TerminalName::JetBrains,
] {
assert!(
!keyboard_capabilities(brand)
.modifier_delivery
.benefits_from_rescue()
);
}
}
}