Skip to main content

forge_terminal/
keys.rs

1//! The named keys an agent may press, and the bytes each one sends.
2//!
3//! **A key is bytes, not a command.** The shell and every program in it see
4//! an ordinary terminal; Enter is its own key, Ctrl-C is 0x03, and the arrows
5//! are the sequences an `xterm-256color` terminal sends. Nothing here names an
6//! agent.
7
8use serde::{Deserialize, Serialize};
9
10/// One named key, pressed in order with the text a call carries.
11#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(rename_all = "snake_case")]
13pub enum Key {
14    Enter,
15    Tab,
16    Escape,
17    Up,
18    Down,
19    Left,
20    Right,
21    Home,
22    End,
23    PageUp,
24    PageDown,
25    Backspace,
26    CtrlC,
27    CtrlD,
28    CtrlZ,
29    CtrlL,
30}
31
32impl Key {
33    /// The bytes this key sends to the PTY.
34    pub const fn bytes(self) -> &'static [u8] {
35        match self {
36            Self::Enter => b"\r",
37            Self::Tab => b"\t",
38            Self::Escape => b"\x1b",
39            Self::Up => b"\x1b[A",
40            Self::Down => b"\x1b[B",
41            Self::Right => b"\x1b[C",
42            Self::Left => b"\x1b[D",
43            Self::Home => b"\x1b[H",
44            Self::End => b"\x1b[F",
45            Self::PageUp => b"\x1b[5~",
46            Self::PageDown => b"\x1b[6~",
47            Self::Backspace => b"\x7f",
48            Self::CtrlC => b"\x03",
49            Self::CtrlD => b"\x04",
50            Self::CtrlZ => b"\x1a",
51            Self::CtrlL => b"\x0c",
52        }
53    }
54
55    /// Parse the wire spelling a tool call carries.
56    pub fn parse(name: &str) -> Option<Self> {
57        Some(match name {
58            "enter" => Self::Enter,
59            "tab" => Self::Tab,
60            "escape" => Self::Escape,
61            "up" => Self::Up,
62            "down" => Self::Down,
63            "left" => Self::Left,
64            "right" => Self::Right,
65            "home" => Self::Home,
66            "end" => Self::End,
67            "page_up" => Self::PageUp,
68            "page_down" => Self::PageDown,
69            "backspace" => Self::Backspace,
70            "ctrl_c" => Self::CtrlC,
71            "ctrl_d" => Self::CtrlD,
72            "ctrl_z" => Self::CtrlZ,
73            "ctrl_l" => Self::CtrlL,
74            _ => return None,
75        })
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    /// **Enter is a carriage return, not a newline.** A terminal's Enter key
84    /// sends `\r`; a `\n` is Ctrl-J and a program that reads raw keys sees a
85    /// different key, and zsh does not accept a line on one.
86    #[test]
87    fn enter_is_a_carriage_return() {
88        assert_eq!(Key::Enter.bytes(), b"\r");
89        assert_ne!(Key::Enter.bytes(), b"\n");
90    }
91
92    /// The control keys are the terminal's own bytes, so a program sees the
93    /// key a person pressed rather than an escape sequence.
94    #[test]
95    fn the_control_keys_are_their_own_bytes() {
96        assert_eq!(Key::CtrlC.bytes(), b"\x03");
97        assert_eq!(Key::CtrlD.bytes(), b"\x04");
98        assert_eq!(Key::CtrlZ.bytes(), b"\x1a");
99        assert_eq!(Key::CtrlL.bytes(), b"\x0c");
100    }
101
102    /// Every wire spelling parses back to the key it names, so a tool call's
103    /// `keys` array cannot silently drop one.
104    #[test]
105    fn every_wire_spelling_parses() {
106        for (name, key) in [
107            ("enter", Key::Enter),
108            ("tab", Key::Tab),
109            ("escape", Key::Escape),
110            ("up", Key::Up),
111            ("down", Key::Down),
112            ("left", Key::Left),
113            ("right", Key::Right),
114            ("home", Key::Home),
115            ("end", Key::End),
116            ("page_up", Key::PageUp),
117            ("page_down", Key::PageDown),
118            ("backspace", Key::Backspace),
119            ("ctrl_c", Key::CtrlC),
120            ("ctrl_d", Key::CtrlD),
121            ("ctrl_z", Key::CtrlZ),
122            ("ctrl_l", Key::CtrlL),
123        ] {
124            assert_eq!(Key::parse(name), Some(key), "{name}");
125        }
126        assert_eq!(Key::parse("ctrl_q"), None, "an unknown key is refused");
127    }
128}