yoda: protocole clavier kitty, Shift+Entree pour Claude Code et Modjo

Claude Code (>= 2.1.269) n'active Shift+Entree que si le terminal repond
a CSI ? u. alacritty_terminal sait tenir la pile des modes et repondre,
mais seulement avec kitty_keyboard, et n'encode aucune touche : Shift+
Entree partait en \r, identique a Entree.

- term::Config : kitty_keyboard active.
- kitty_keyboard : encodeur du frontal (spec kitty, decoupe de l'encodeur
  d'Alacritty), utilise seulement quand l'application au premier plan a
  pousse le drapeau 1 ou 8 ; sinon l'encodage legacy reste seul. Gere
  desambiguisation, types d'evenements (repetition, relachement),
  touches alternatives, toutes les touches, texte associe, pave
  numerique, F1-F24, touches de modification. Shift+Entree -> CSI 13;2u.
- terminal_box : raccourcis de l'appli toujours prioritaires ;
  Shift+PageUp/PageDown/Home/End et Echap sur selection gardent leur
  comportement local ; relachements envoyes si le drapeau 2 est pose.
- Legacy : Shift+Entree envoie LF (comme Ctrl+J) au lieu de CR. rustyline
  le lit comme Ctrl+J, la touche de saut de ligne de Modjo/Goose ; pour
  un shell c'est toujours Entree.

Connu, non corrige ici : alacritty_terminal 0.25.1 push_keyboard_mode vide
title_stack au lieu de la pile des modes au-dela de 4096 empilements
(panique du fil PTY de l'onglet seulement, panic=unwind).

10 tests, dont la reponse a CSI ? u et l'application de CSI > 5 u.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lionel DARNIS 2026-10-01 13:32:23 +02:00
parent 2414aaece3
commit 52b37cc2f8
3 changed files with 808 additions and 2 deletions

697
src/kitty_keyboard.rs Normal file
View file

@ -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 : <https://sw.kovidgoyal.net/kitty/keyboard-protocol/>. 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<u8> {
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<Vec<u8>> {
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<String> = 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<Mutex<Vec<String>>>);
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[<u");
assert!(!active(*term.mode()));
}
}

View file

@ -79,6 +79,8 @@ mod desktop_notify;
use osc_tap::OscEvent;
mod osc_tap;
mod kitty_keyboard;
use terminal_box::terminal_box;
use crate::dnd::DndDrop;
@ -311,6 +313,9 @@ fn main() -> Result<(), Box<dyn Error>> {
// 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

View file

@ -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 => {