diff --git a/src/kitty_keyboard.rs b/src/kitty_keyboard.rs
new file mode 100644
index 0000000..df24628
--- /dev/null
+++ b/src/kitty_keyboard.rs
@@ -0,0 +1,697 @@
+// SPDX-License-Identifier: GPL-3.0-only
+
+//! Protocole clavier kitty (yoda).
+//!
+//! Avec `kitty_keyboard` actif, alacritty_terminal tient la pile des modes
+//! (`CSI > n u`, `CSI < n u`, `CSI = n ; m u`) et répond à la requête
+//! `CSI ? u`. Il n'encode en revanche aucune touche : c'est le rôle du frontal,
+//! donc de ce module. Il ne sert que quand l'application au premier plan a
+//! demandé le protocole ; sinon l'encodage legacy de `terminal_box` s'applique
+//! seul.
+//!
+//! C'est ce qui donne Shift+Entrée (`CSI 13;2u`) à Claude Code, qui interroge
+//! le terminal avec `CSI ? u` au démarrage.
+//!
+//! Spécification : . La
+//! découpe suit l'encodeur du frontal Alacritty (`alacritty/src/input/keyboard.rs`).
+//!
+//! Limites : les drapeaux 2 (types d'événements), 4 (touches alternatives) et
+//! 16 (texte associé) ne sont honorés qu'avec le drapeau 1 (désambiguïsation)
+//! ou 8 (toutes les touches), ce que font toutes les applications connues.
+//! La touche de la disposition de base (3e champ du drapeau 4) n'est pas
+//! envoyée, et Caps Lock / Num Lock ne figurent pas dans les modificateurs
+//! (iced ne les expose pas).
+
+use alacritty_terminal::term::TermMode;
+use cosmic::iced::keyboard::{Key, Location, Modifiers, key::Named};
+
+/// Nature de l'événement clavier.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum KeyAction {
+ Press,
+ Repeat,
+ Release,
+}
+
+/// Événement clavier tel que livré par iced.
+pub struct KeyInput<'a> {
+ /// Touche sans aucun modificateur (`key_without_modifiers` de winit).
+ pub key: &'a Key,
+ /// Touche avec Shift (et AltGr) appliqués, mais pas Ctrl (`logical_key`).
+ pub modified_key: &'a Key,
+ pub location: Location,
+ pub modifiers: Modifiers,
+ /// Texte produit, tous modificateurs appliqués.
+ pub text: Option<&'a str>,
+ pub action: KeyAction,
+}
+
+const SHIFT: u8 = 1;
+const ALT: u8 = 2;
+const CTRL: u8 = 4;
+const SUPER: u8 = 8;
+
+/// Le protocole kitty pilote-t-il l'encodage des touches ?
+pub fn active(mode: TermMode) -> bool {
+ mode.intersects(TermMode::DISAMBIGUATE_ESC_CODES | TermMode::REPORT_ALL_KEYS_AS_ESC)
+}
+
+/// Octets à envoyer au PTY quand [`active`] est vrai. Vide : rien à envoyer.
+pub fn encode(input: &KeyInput, mode: TermMode) -> Vec {
+ let all = mode.contains(TermMode::REPORT_ALL_KEYS_AS_ESC);
+ let mods = modifier_bits(input);
+ let named = match input.modified_key {
+ Key::Named(named) => Some(*named),
+ _ => None,
+ };
+
+ if input.action == KeyAction::Release {
+ if !mode.contains(TermMode::REPORT_EVENT_TYPES) {
+ return Vec::new();
+ }
+ // Pas de relâchement d'Entrée, Tab et Retour arrière hors « toutes les
+ // touches » : le shell qui reprend la main après un plantage les
+ // recevrait.
+ if !all && matches!(named, Some(Named::Enter | Named::Tab | Named::Backspace)) {
+ return Vec::new();
+ }
+ return build_sequence(input, mode, mods).unwrap_or_default();
+ }
+
+ // Sans modificateur, Entrée, Tab et Retour arrière gardent leurs octets
+ // legacy (spécification : un `reset` doit rester tapable). L'Entrée du
+ // pavé numérique, elle, a son code dédié.
+ if !all && mods == 0 && input.location != Location::Numpad {
+ match named {
+ Some(Named::Enter) => return b"\r".to_vec(),
+ Some(Named::Tab) => return b"\t".to_vec(),
+ Some(Named::Backspace) => return b"\x7f".to_vec(),
+ _ => {}
+ }
+ }
+
+ let text = printable_text(input);
+ let as_sequence = all
+ || text.is_none()
+ || named == Some(Named::Escape)
+ || (mods != 0 && mods != SHIFT)
+ || (mods == SHIFT && matches!(named, Some(Named::Enter | Named::Tab | Named::Backspace)));
+
+ if as_sequence {
+ build_sequence(input, mode, mods).unwrap_or_default()
+ } else {
+ text.unwrap_or_default().as_bytes().to_vec()
+ }
+}
+
+/// Texte imprimable produit par la touche, s'il y en a un.
+fn printable_text<'a>(input: &KeyInput<'a>) -> Option<&'a str> {
+ input
+ .text
+ .filter(|text| !text.is_empty() && !text.chars().any(char::is_control))
+}
+
+fn modifier_bits(input: &KeyInput) -> u8 {
+ let mut bits = 0;
+ if input.modifiers.shift() {
+ bits |= SHIFT;
+ }
+ if input.modifiers.alt() {
+ bits |= ALT;
+ }
+ if input.modifiers.control() {
+ bits |= CTRL;
+ }
+ if input.modifiers.logo() {
+ bits |= SUPER;
+ }
+ // Pour une touche de modification, l'état rapporté est celui qui suit
+ // l'événement : le bit est posé à l'appui, retiré au relâchement.
+ let own = match input.modified_key {
+ Key::Named(Named::Shift) => SHIFT,
+ Key::Named(Named::Alt) => ALT,
+ Key::Named(Named::Control) => CTRL,
+ Key::Named(Named::Super) => SUPER,
+ _ => 0,
+ };
+ if input.action == KeyAction::Release {
+ bits & !own
+ } else {
+ bits | own
+ }
+}
+
+/// Terminateur d'une séquence : `u` pour les codes kitty, sinon le final
+/// legacy (`A`, `~`…) que le protocole conserve pour les touches de fonction.
+#[derive(Clone, Copy)]
+enum Final {
+ Kitty,
+ Legacy(char),
+}
+
+fn build_sequence(input: &KeyInput, mode: TermMode, mods: u8) -> Option> {
+ let event_type = mode.contains(TermMode::REPORT_EVENT_TYPES)
+ && matches!(input.action, KeyAction::Repeat | KeyAction::Release);
+ let associated_text = printable_text(input).filter(|_| {
+ mode.contains(TermMode::REPORT_ASSOCIATED_TEXT) && input.action != KeyAction::Release
+ });
+ // Le paramètre par défaut vaut 1 : `CSI A` plutôt que `CSI 1 A`.
+ let explicit = mods != 0 || event_type || associated_text.is_some();
+
+ let (payload, terminator) = numpad(input)
+ .or_else(|| functional(input, explicit))
+ .or_else(|| special(input, mode))
+ .or_else(|| textual(input, mode, associated_text.is_some()))?;
+
+ let mut sequence = format!("\x1b[{payload}");
+ if explicit {
+ sequence.push_str(&format!(";{}", mods + 1));
+ }
+ if event_type {
+ let kind = match input.action {
+ KeyAction::Press => '1',
+ KeyAction::Repeat => '2',
+ KeyAction::Release => '3',
+ };
+ sequence.push(':');
+ sequence.push(kind);
+ }
+ if let Some(text) = associated_text {
+ let codepoints: Vec = text.chars().map(|c| u32::from(c).to_string()).collect();
+ sequence.push(';');
+ sequence.push_str(&codepoints.join(":"));
+ }
+ sequence.push(match terminator {
+ Final::Kitty => 'u',
+ Final::Legacy(c) => c,
+ });
+ Some(sequence.into_bytes())
+}
+
+/// Pavé numérique : codes kitty dédiés (57399 et suivants).
+fn numpad(input: &KeyInput) -> Option<(String, Final)> {
+ if input.location != Location::Numpad {
+ return None;
+ }
+ let code = match input.modified_key {
+ Key::Character(c) => match c.as_str() {
+ "0" => 57399,
+ "1" => 57400,
+ "2" => 57401,
+ "3" => 57402,
+ "4" => 57403,
+ "5" => 57404,
+ "6" => 57405,
+ "7" => 57406,
+ "8" => 57407,
+ "9" => 57408,
+ "." | "," => 57409,
+ "/" => 57410,
+ "*" => 57411,
+ "-" => 57412,
+ "+" => 57413,
+ "=" => 57415,
+ _ => return None,
+ },
+ Key::Named(named) => match named {
+ Named::Enter => 57414,
+ Named::ArrowLeft => 57417,
+ Named::ArrowRight => 57418,
+ Named::ArrowUp => 57419,
+ Named::ArrowDown => 57420,
+ Named::PageUp => 57421,
+ Named::PageDown => 57422,
+ Named::Home => 57423,
+ Named::End => 57424,
+ Named::Insert => 57425,
+ Named::Delete => 57426,
+ Named::Clear => 57427,
+ _ => return None,
+ },
+ Key::Unidentified => return None,
+ };
+ Some((code.to_string(), Final::Kitty))
+}
+
+/// Touches de fonction : finals legacy conservés par le protocole, sauf F3
+/// (`CSI R` serait pris pour un rapport de position du curseur) et F13+.
+fn functional(input: &KeyInput, explicit: bool) -> Option<(String, Final)> {
+ let Key::Named(named) = input.modified_key else {
+ return None;
+ };
+ let one = if explicit { "1" } else { "" };
+ let (payload, terminator) = match named {
+ Named::ArrowUp => (one, Final::Legacy('A')),
+ Named::ArrowDown => (one, Final::Legacy('B')),
+ Named::ArrowRight => (one, Final::Legacy('C')),
+ Named::ArrowLeft => (one, Final::Legacy('D')),
+ Named::End => (one, Final::Legacy('F')),
+ Named::Home => (one, Final::Legacy('H')),
+ Named::F1 => (one, Final::Legacy('P')),
+ Named::F2 => (one, Final::Legacy('Q')),
+ Named::F3 => ("13", Final::Legacy('~')),
+ Named::F4 => (one, Final::Legacy('S')),
+ Named::Insert => ("2", Final::Legacy('~')),
+ Named::Delete => ("3", Final::Legacy('~')),
+ Named::PageUp => ("5", Final::Legacy('~')),
+ Named::PageDown => ("6", Final::Legacy('~')),
+ Named::F5 => ("15", Final::Legacy('~')),
+ Named::F6 => ("17", Final::Legacy('~')),
+ Named::F7 => ("18", Final::Legacy('~')),
+ Named::F8 => ("19", Final::Legacy('~')),
+ Named::F9 => ("20", Final::Legacy('~')),
+ Named::F10 => ("21", Final::Legacy('~')),
+ Named::F11 => ("23", Final::Legacy('~')),
+ Named::F12 => ("24", Final::Legacy('~')),
+ _ => {
+ let code = match named {
+ Named::F13 => 57376,
+ Named::F14 => 57377,
+ Named::F15 => 57378,
+ Named::F16 => 57379,
+ Named::F17 => 57380,
+ Named::F18 => 57381,
+ Named::F19 => 57382,
+ Named::F20 => 57383,
+ Named::F21 => 57384,
+ Named::F22 => 57385,
+ Named::F23 => 57386,
+ Named::F24 => 57387,
+ Named::CapsLock => 57358,
+ Named::ScrollLock => 57359,
+ Named::NumLock => 57360,
+ Named::PrintScreen => 57361,
+ Named::Pause => 57362,
+ Named::ContextMenu => 57363,
+ Named::MediaPlay => 57428,
+ Named::MediaPause => 57429,
+ Named::MediaPlayPause => 57430,
+ Named::MediaStop => 57432,
+ Named::MediaFastForward => 57433,
+ Named::MediaRewind => 57434,
+ Named::MediaTrackNext => 57435,
+ Named::MediaTrackPrevious => 57436,
+ Named::MediaRecord => 57437,
+ Named::AudioVolumeDown => 57438,
+ Named::AudioVolumeUp => 57439,
+ Named::AudioVolumeMute => 57440,
+ _ => return None,
+ };
+ return Some((code.to_string(), Final::Kitty));
+ }
+ };
+ Some((payload.to_string(), terminator))
+}
+
+/// Touches de contrôle (Entrée, Tab, Échap, Retour arrière, Espace) et, avec
+/// « toutes les touches », les touches de modification elles-mêmes.
+fn special(input: &KeyInput, mode: TermMode) -> Option<(String, Final)> {
+ let code = match input.modified_key {
+ Key::Named(Named::Enter) => 13,
+ Key::Named(Named::Tab) => 9,
+ Key::Named(Named::Escape) => 27,
+ Key::Named(Named::Backspace) => 127,
+ Key::Character(c) if c == " " => 32,
+ Key::Named(modifier) if mode.contains(TermMode::REPORT_ALL_KEYS_AS_ESC) => {
+ let right = input.location == Location::Right;
+ match (modifier, right) {
+ (Named::Shift, false) => 57441,
+ (Named::Control, false) => 57442,
+ (Named::Alt, false) => 57443,
+ (Named::Super, false) => 57444,
+ (Named::Hyper, false) => 57445,
+ (Named::Meta, false) => 57446,
+ (Named::Shift, true) => 57447,
+ (Named::Control, true) => 57448,
+ (Named::Alt, true) => 57449,
+ (Named::Super, true) => 57450,
+ (Named::Hyper, true) => 57451,
+ (Named::Meta, true) => 57452,
+ (Named::AltGraph, _) => 57453,
+ _ => return None,
+ }
+ }
+ _ => return None,
+ };
+ Some((code.to_string(), Final::Kitty))
+}
+
+/// Touches de caractère : code de la touche non décalée, suivi de la touche
+/// décalée si le drapeau 4 le demande (`CSI 97:65;6u` pour Ctrl+Shift+A).
+fn textual(input: &KeyInput, mode: TermMode, has_text: bool) -> Option<(String, Final)> {
+ let Key::Character(shifted) = input.modified_key else {
+ return None;
+ };
+ let mut chars = shifted.chars();
+ let (Some(shifted_char), None) = (chars.next(), chars.next()) else {
+ // Plusieurs caractères pour une touche : seul « toutes les touches »
+ // avec texte associé sait le dire, par le code 0.
+ return (mode.contains(TermMode::REPORT_ALL_KEYS_AS_ESC) && has_text)
+ .then(|| ("0".to_string(), Final::Kitty));
+ };
+
+ let unshifted_char = if input.modifiers.shift() {
+ shifted_char.to_lowercase().next().unwrap_or(shifted_char)
+ } else {
+ shifted_char
+ };
+ let mut code = u32::from(unshifted_char);
+ let alternate = u32::from(shifted_char);
+ // `!` n'a pas de minuscule : la touche de base est celle sans modificateur
+ // (`1` en QWERTY ; `&` pour le `1` de l'AZERTY).
+ if code == alternate
+ && let Key::Character(base) = input.key
+ && let Some(base_char) = base.chars().next()
+ {
+ code = u32::from(base_char);
+ }
+
+ let payload = if mode.contains(TermMode::REPORT_ALTERNATE_KEYS) && alternate != code {
+ format!("{code}:{alternate}")
+ } else {
+ code.to_string()
+ };
+ Some((payload, Final::Kitty))
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ const DISAMBIGUATE: TermMode = TermMode::DISAMBIGUATE_ESC_CODES;
+
+ fn ch(c: &str) -> Key {
+ Key::Character(c.into())
+ }
+
+ fn named(n: Named) -> Key {
+ Key::Named(n)
+ }
+
+ fn press(
+ key: Key,
+ modified: Key,
+ modifiers: Modifiers,
+ text: Option<&str>,
+ mode: TermMode,
+ ) -> String {
+ event(key, modified, modifiers, text, KeyAction::Press, mode)
+ }
+
+ fn event(
+ key: Key,
+ modified: Key,
+ modifiers: Modifiers,
+ text: Option<&str>,
+ action: KeyAction,
+ mode: TermMode,
+ ) -> String {
+ let input = KeyInput {
+ key: &key,
+ modified_key: &modified,
+ location: Location::Standard,
+ modifiers,
+ text,
+ action,
+ };
+ String::from_utf8(encode(&input, mode)).unwrap()
+ }
+
+ #[test]
+ fn shift_enter_for_claude_code() {
+ let enter = || named(Named::Enter);
+ assert_eq!(
+ press(enter(), enter(), Modifiers::SHIFT, Some("\r"), DISAMBIGUATE),
+ "\x1b[13;2u"
+ );
+ assert_eq!(
+ press(enter(), enter(), Modifiers::CTRL, Some("\r"), DISAMBIGUATE),
+ "\x1b[13;5u"
+ );
+ // Entrée seule reste `\r`
+ assert_eq!(
+ press(enter(), enter(), Modifiers::empty(), Some("\r"), DISAMBIGUATE),
+ "\r"
+ );
+ }
+
+ #[test]
+ fn escape_tab_backspace() {
+ let esc = || named(Named::Escape);
+ assert_eq!(
+ press(esc(), esc(), Modifiers::empty(), Some("\x1b"), DISAMBIGUATE),
+ "\x1b[27u"
+ );
+ let tab = || named(Named::Tab);
+ assert_eq!(
+ press(tab(), tab(), Modifiers::empty(), Some("\t"), DISAMBIGUATE),
+ "\t"
+ );
+ assert_eq!(
+ press(tab(), tab(), Modifiers::SHIFT, Some("\t"), DISAMBIGUATE),
+ "\x1b[9;2u"
+ );
+ let bs = || named(Named::Backspace);
+ assert_eq!(
+ press(bs(), bs(), Modifiers::empty(), Some("\x08"), DISAMBIGUATE),
+ "\x7f"
+ );
+ assert_eq!(
+ press(bs(), bs(), Modifiers::CTRL, Some("\x08"), DISAMBIGUATE),
+ "\x1b[127;5u"
+ );
+ }
+
+ #[test]
+ fn text_keys() {
+ // Texte simple ou décalé : envoyé tel quel
+ assert_eq!(
+ press(ch("a"), ch("a"), Modifiers::empty(), Some("a"), DISAMBIGUATE),
+ "a"
+ );
+ assert_eq!(
+ press(ch("a"), ch("A"), Modifiers::SHIFT, Some("A"), DISAMBIGUATE),
+ "A"
+ );
+ assert_eq!(
+ press(ch("é"), ch("é"), Modifiers::empty(), Some("é"), DISAMBIGUATE),
+ "é"
+ );
+ // Ctrl et Alt : séquences
+ assert_eq!(
+ press(ch("c"), ch("c"), Modifiers::CTRL, Some("\x03"), DISAMBIGUATE),
+ "\x1b[99;5u"
+ );
+ assert_eq!(
+ press(ch("a"), ch("a"), Modifiers::ALT, Some("a"), DISAMBIGUATE),
+ "\x1b[97;3u"
+ );
+ assert_eq!(
+ press(ch(" "), ch(" "), Modifiers::CTRL, Some("\0"), DISAMBIGUATE),
+ "\x1b[32;5u"
+ );
+ }
+
+ #[test]
+ fn alternate_keys() {
+ let mode = DISAMBIGUATE | TermMode::REPORT_ALTERNATE_KEYS;
+ assert_eq!(
+ press(
+ ch("a"),
+ ch("A"),
+ Modifiers::CTRL | Modifiers::SHIFT,
+ Some("\x01"),
+ mode
+ ),
+ "\x1b[97:65;6u"
+ );
+ // AZERTY : Ctrl+Shift+& donne « 1 »
+ assert_eq!(
+ press(
+ ch("&"),
+ ch("1"),
+ Modifiers::CTRL | Modifiers::SHIFT,
+ None,
+ mode
+ ),
+ "\x1b[38:49;6u"
+ );
+ // Sans le drapeau 4, pas d'alternative
+ assert_eq!(
+ press(
+ ch("a"),
+ ch("A"),
+ Modifiers::CTRL | Modifiers::SHIFT,
+ Some("\x01"),
+ DISAMBIGUATE
+ ),
+ "\x1b[97;6u"
+ );
+ }
+
+ #[test]
+ fn functional_keys() {
+ let up = || named(Named::ArrowUp);
+ assert_eq!(
+ press(up(), up(), Modifiers::empty(), None, DISAMBIGUATE),
+ "\x1b[A"
+ );
+ assert_eq!(
+ press(up(), up(), Modifiers::CTRL, None, DISAMBIGUATE),
+ "\x1b[1;5A"
+ );
+ let f3 = || named(Named::F3);
+ assert_eq!(
+ press(f3(), f3(), Modifiers::empty(), None, DISAMBIGUATE),
+ "\x1b[13~"
+ );
+ let del = || named(Named::Delete);
+ assert_eq!(
+ press(del(), del(), Modifiers::SHIFT, None, DISAMBIGUATE),
+ "\x1b[3;2~"
+ );
+ }
+
+ #[test]
+ fn event_types() {
+ let mode = DISAMBIGUATE | TermMode::REPORT_EVENT_TYPES;
+ assert_eq!(
+ event(
+ ch("a"),
+ ch("a"),
+ Modifiers::empty(),
+ None,
+ KeyAction::Release,
+ mode
+ ),
+ "\x1b[97;1:3u"
+ );
+ assert_eq!(
+ event(
+ ch("a"),
+ ch("a"),
+ Modifiers::CTRL,
+ Some("\x01"),
+ KeyAction::Repeat,
+ mode
+ ),
+ "\x1b[97;5:2u"
+ );
+ // Pas de relâchement d'Entrée hors « toutes les touches »
+ let enter = || named(Named::Enter);
+ assert_eq!(
+ event(
+ enter(),
+ enter(),
+ Modifiers::empty(),
+ None,
+ KeyAction::Release,
+ mode
+ ),
+ ""
+ );
+ // Ni de relâchement du tout sans le drapeau 2
+ assert_eq!(
+ event(
+ ch("a"),
+ ch("a"),
+ Modifiers::empty(),
+ None,
+ KeyAction::Release,
+ DISAMBIGUATE
+ ),
+ ""
+ );
+ }
+
+ #[test]
+ fn report_all_keys() {
+ let mode = TermMode::REPORT_ALL_KEYS_AS_ESC | TermMode::REPORT_ASSOCIATED_TEXT;
+ assert_eq!(
+ press(ch("a"), ch("a"), Modifiers::empty(), Some("a"), mode),
+ "\x1b[97;1;97u"
+ );
+ let enter = || named(Named::Enter);
+ assert_eq!(
+ press(enter(), enter(), Modifiers::empty(), Some("\r"), mode),
+ "\x1b[13u"
+ );
+ // Touche de modification : son propre bit est posé à l'appui
+ let shift = || named(Named::Shift);
+ assert_eq!(
+ press(shift(), shift(), Modifiers::empty(), None, mode),
+ "\x1b[57441;2u"
+ );
+ }
+
+ #[test]
+ fn numpad_keys() {
+ let input = |key: &Key, text| {
+ String::from_utf8(encode(
+ &KeyInput {
+ key,
+ modified_key: key,
+ location: Location::Numpad,
+ modifiers: Modifiers::empty(),
+ text,
+ action: KeyAction::Press,
+ },
+ DISAMBIGUATE,
+ ))
+ .unwrap()
+ };
+ // Un chiffre du pavé produit du texte : envoyé tel quel
+ assert_eq!(input(&ch("5"), Some("5")), "5");
+ // Entrée du pavé n'en produit pas d'imprimable : code dédié
+ assert_eq!(input(&named(Named::Enter), Some("\r")), "\x1b[57414u");
+ }
+
+ #[test]
+ fn only_with_disambiguate_or_all() {
+ assert!(!active(TermMode::empty()));
+ assert!(!active(TermMode::REPORT_EVENT_TYPES));
+ assert!(active(DISAMBIGUATE));
+ assert!(active(TermMode::REPORT_ALL_KEYS_AS_ESC));
+ }
+
+ /// Le terminal répond à `CSI ? u` et applique `CSI > 5 u` dès que
+ /// `kitty_keyboard` est actif : c'est ce que Claude Code interroge.
+ #[test]
+ fn term_reports_and_applies_kitty_modes() {
+ use alacritty_terminal::{
+ Term,
+ event::{Event, EventListener},
+ term::{Config, test::TermSize},
+ vte::ansi::Processor,
+ };
+ use std::sync::{Arc, Mutex};
+
+ #[derive(Clone, Default)]
+ struct Replies(Arc>>);
+ impl EventListener for Replies {
+ fn send_event(&self, event: Event) {
+ if let Event::PtyWrite(text) = event {
+ self.0.lock().unwrap().push(text);
+ }
+ }
+ }
+
+ let replies = Replies::default();
+ let config = Config {
+ kitty_keyboard: true,
+ ..Config::default()
+ };
+ let mut term = Term::new(config, &TermSize::new(80, 24), replies.clone());
+ let mut parser: Processor = Processor::new();
+ parser.advance(&mut term, b"\x1b[?u\x1b[>5u\x1b[?u");
+
+ assert_eq!(*replies.0.lock().unwrap(), ["\x1b[?0u", "\x1b[?5u"]);
+ let mode = *term.mode();
+ assert!(mode.contains(TermMode::DISAMBIGUATE_ESC_CODES));
+ assert!(mode.contains(TermMode::REPORT_ALTERNATE_KEYS));
+ assert!(active(mode));
+
+ parser.advance(&mut term, b"\x1b[ Result<(), Box> {
// Terminal config setup
let term_config = term::Config {
scrolling_history: 100_000,
+ // [yoda] Protocole clavier kitty : alacritty_terminal tient la pile des
+ // modes et répond à `CSI ? u`, l'encodage est dans `kitty_keyboard`.
+ kitty_keyboard: true,
..term::Config::default()
};
// Set up environmental variables for terminal
diff --git a/src/terminal_box.rs b/src/terminal_box.rs
index 1df9ae3..be73890 100644
--- a/src/terminal_box.rs
+++ b/src/terminal_box.rs
@@ -47,7 +47,7 @@ use std::{
};
use crate::{
- Action, Terminal, TerminalScroll,
+ Action, Terminal, TerminalScroll, kitty_keyboard,
menu::MenuState,
terminal::{BlockFill, Metadata},
};
@@ -940,6 +940,7 @@ where
let is_app_cursor = terminal.term.lock().mode().contains(TermMode::APP_CURSOR);
let is_mouse_mode = terminal.term.lock().mode().intersects(TermMode::MOUSE_MODE);
+ let term_mode = *terminal.term.lock().mode();
match event {
Event::Window(event) => match event {
cosmic::iced::window::Event::Focused => {
@@ -979,6 +980,101 @@ where
}
_ => {}
},
+ // [yoda] Protocole clavier kitty, quand l'application au premier plan
+ // l'a demandé (voir `kitty_keyboard`) ; sinon encodage legacy plus bas.
+ Event::Keyboard(KeyEvent::KeyPressed {
+ key,
+ modified_key,
+ physical_key,
+ location,
+ modifiers,
+ text,
+ repeat,
+ }) if state.is_focused && kitty_keyboard::active(term_mode) => {
+ for key_bind in self.key_binds.keys() {
+ if key_bind.matches(*modifiers, key, Some(physical_key)) {
+ shell.capture_event();
+ return;
+ }
+ }
+
+ // Comportements locaux conservés, comme en legacy : Shift+PageUp…
+ // fait défiler l'historique, Échap annule une sélection.
+ let local_scroll = match modified_key {
+ Key::Named(Named::PageUp) if modifiers.shift() => Some(TerminalScroll::PageUp),
+ Key::Named(Named::PageDown) if modifiers.shift() => {
+ Some(TerminalScroll::PageDown)
+ }
+ Key::Named(Named::Home) if modifiers.shift() => Some(TerminalScroll::Top),
+ Key::Named(Named::End) if modifiers.shift() => Some(TerminalScroll::Bottom),
+ _ => None,
+ };
+ if let Some(scroll) = local_scroll {
+ terminal.scroll(scroll);
+ shell.capture_event();
+ return;
+ }
+ if *modified_key == Key::Named(Named::Escape) {
+ let had_selection = terminal.term.lock().selection.take().is_some();
+ if had_selection {
+ terminal.update();
+ shell.capture_event();
+ return;
+ }
+ }
+
+ let input = kitty_keyboard::KeyInput {
+ key,
+ modified_key,
+ location: *location,
+ modifiers: *modifiers,
+ text: text.as_deref(),
+ action: if *repeat {
+ kitty_keyboard::KeyAction::Repeat
+ } else {
+ kitty_keyboard::KeyAction::Press
+ },
+ };
+ let bytes = kitty_keyboard::encode(&input, term_mode);
+ if !bytes.is_empty() {
+ // Une touche de modification seule (« toutes les touches »)
+ // ne ramène pas la vue en bas.
+ let modifier_only = matches!(
+ modified_key,
+ Key::Named(Named::Shift | Named::Control | Named::Alt | Named::Super)
+ );
+ if modifier_only {
+ terminal.input_no_scroll(bytes);
+ } else {
+ terminal.input_scroll(bytes);
+ }
+ shell.capture_event();
+ }
+ }
+ Event::Keyboard(KeyEvent::KeyReleased {
+ key,
+ modified_key,
+ location,
+ modifiers,
+ ..
+ }) if state.is_focused
+ && kitty_keyboard::active(term_mode)
+ && term_mode.contains(TermMode::REPORT_EVENT_TYPES) =>
+ {
+ let input = kitty_keyboard::KeyInput {
+ key,
+ modified_key,
+ location: *location,
+ modifiers: *modifiers,
+ text: None,
+ action: kitty_keyboard::KeyAction::Release,
+ };
+ let bytes = kitty_keyboard::encode(&input, term_mode);
+ if !bytes.is_empty() {
+ terminal.input_no_scroll(bytes);
+ shell.capture_event();
+ }
+ }
Event::Keyboard(KeyEvent::KeyPressed {
key: Key::Named(named),
physical_key,
@@ -1094,7 +1190,15 @@ where
shell.capture_event();
}
Named::Enter => {
- terminal.input_scroll(format!("{}{}", alt_prefix, "\x0D").into_bytes());
+ // [yoda] Shift+Entrée envoie LF (`\n`, comme Ctrl+J) : saut de
+ // ligne dans les éditeurs qui ne parlent pas le protocole kitty
+ // (rustyline de Modjo/Goose) ; pour un shell, c'est Entrée.
+ let code = if modifiers.shift() && !modifiers.control() {
+ "\x0A"
+ } else {
+ "\x0D"
+ };
+ terminal.input_scroll(format!("{alt_prefix}{code}").into_bytes());
shell.capture_event();
}
Named::Escape => {