Rebrand to Galaxy, major improvements to Bedrock support, still needs some TLC though

This commit is contained in:
Ryan Ward
2026-05-07 11:29:34 -05:00
parent f4e2475c60
commit a41cbd8cc7
2433 changed files with 14208 additions and 9409 deletions
@@ -0,0 +1,325 @@
use galaxyui::keymap::Keystroke;
use galaxyui::platform::keyboard::KeyCode;
use galaxyui::platform::OperatingSystem;
use super::{ModeProvider, TermMode};
/// Checks whether the Kitty keyboard protocol requires CSI u encoding for this
/// keystroke, and if so, returns the encoded sequence.
///
/// Under REPORT_ALL_KEYS_AS_ESCAPE (flag 8), all keys use CSI u.
/// Under DISAMBIGUATE_ESCAPE_CODES (flag 1), only ambiguous keys use CSI u.
/// Returns None if CSI u is not needed (caller should fall back to legacy encoding).
pub(super) fn maybe_convert_keystroke_to_csi_u(
keystroke: &Keystroke,
key_without_modifiers: Option<&str>,
chars: Option<&str>,
mode_provider: &dyn ModeProvider,
) -> Option<Vec<u8>> {
if !mode_provider.is_term_mode_set(TermMode::KEYBOARD_PROTOCOL) {
return None;
}
// Under the DISAMBIGUATE_ESCAPE_CODES flag (flag 1), the Kitty keyboard
// protocol says to use CSI u encoding for keys that would otherwise be
// ambiguous in legacy terminal encoding. This includes:
// - The Escape key (ESC byte 0x1B is also the start of all escape sequences)
// - Modified keys where the modifier is lost in legacy encoding (e.g.,
// Ctrl+A → C0 code 0x01, Alt+a → ESC a on non-macOS)
//
// On macOS, Alt is excluded because Option generates composed characters
// (e.g., Option+a → å) via the IME rather than acting as a modifier.
//
// See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/#disambiguate
let mut is_ambiguous = keystroke.key == "escape" || keystroke.ctrl || keystroke.meta;
if !OperatingSystem::get().is_mac() {
is_ambiguous = is_ambiguous || keystroke.alt;
}
// Shift alone is NOT ambiguous for printable keys — Shift changes the
// character itself (e.g., Shift+a → A). But for functional keys like
// Enter, Tab, and Backspace, legacy encoding drops Shift entirely
// (Shift+Enter sends the same bytes as Enter), making them ambiguous.
if keystroke.shift {
is_ambiguous =
is_ambiguous || matches!(keystroke.key.as_str(), "enter" | "tab" | "backspace");
}
// With flag 8 (REPORT_ALL_KEYS_AS_ESC): use CSI u for all keys.
// With flag 1 (DISAMBIGUATE_ESC_CODES): use CSI u only when ambiguous.
let should_use_csi_u = mode_provider.is_term_mode_set(TermMode::KEYBOARD_REPORT_ALL_AS_ESCAPE)
|| (mode_provider.is_term_mode_set(TermMode::KEYBOARD_DISAMBIGUATE_ESCAPE) && is_ambiguous);
if !should_use_csi_u {
return None;
}
keystroke_to_csi_u(keystroke, key_without_modifiers, chars, mode_provider)
}
/// Encodes a keystroke to a CSI u escape sequence for the Kitty keyboard protocol.
///
/// Full format: CSI unicode-key-code[:shifted-key] ; modifiers[:event_type] ; text-as-codepoints u
/// where modifiers is: 1 + (shift ? 1 : 0) + (alt ? 2 : 0) + (ctrl ? 4 : 0) + (super ? 8 : 0)
///
/// Key codes follow the Kitty protocol specification:
/// - Standard keys use their Unicode codepoints (Enter=13, Tab=9, etc.)
/// - Function keys F13-F35 use codes 57376-57398
/// - When REPORT_ALTERNATE_KEYS (flag 4) is active and shift is held, the shifted key
/// code is appended after a colon (e.g., `97:65` for shift+a).
/// - When REPORT_ASSOCIATED_TEXT (flag 16) is active, the OS-provided text (`chars`)
/// is appended as a colon-separated list of Unicode codepoints (e.g., `;65` for "A").
/// - Event type encoding (press=1, repeat=2, release=3) is omitted for press events
/// since press is the default. Repeat/release are not yet handled here; see
/// `modifier_key_to_csi_u` for modifier key press/release.
///
/// See functional key definitions: https://sw.kovidgoyal.net/kitty/keyboard-protocol/#functional
///
/// Returns None if the key cannot be encoded as a CSI u sequence.
fn keystroke_to_csi_u(
keystroke: &Keystroke,
key_without_modifiers: Option<&str>,
chars: Option<&str>,
mode_provider: &(impl ModeProvider + ?Sized),
) -> Option<Vec<u8>> {
let report_alternate = mode_provider.is_term_mode_set(TermMode::KEYBOARD_REPORT_ALTERNATE_KEYS);
let report_text = mode_provider.is_term_mode_set(TermMode::KEYBOARD_REPORT_ASSOCIATED_TEXT);
// Track the original (possibly shifted) character for alternate key / text reporting.
let original_char: Option<char> = match keystroke.key.as_str() {
key if key.chars().count() == 1 => key.chars().next(),
_ => None,
};
// Map keys to their key codes following the Kitty protocol specification.
let key_code: u32 = match keystroke.key.as_str() {
// Control characters (C0 codes)
"enter" => 13,
"tab" => 9,
"escape" => 27,
"backspace" => 127,
"space" => 32,
// Function keys F13-F35 (Kitty-specific codes).
// F1-F12 are intentionally omitted: the Kitty spec keeps their legacy encoding
// format (SS3 P/Q/R/S for F1-F4, CSI <code> ~ for F5-F12) rather than CSI u.
// They fall through to the legacy escape sequence encoding which already handles
// modifier encoding correctly (CSI 1;mod P/Q/R/S for F1-F4 with modifiers).
"f13" => 57376,
"f14" => 57377,
"f15" => 57378,
"f16" => 57379,
"f17" => 57380,
"f18" => 57381,
"f19" => 57382,
"f20" => 57383,
"f21" => 57384,
"f22" => 57385,
"f23" => 57386,
"f24" => 57387,
"f25" => 57388,
"f26" => 57389,
"f27" => 57390,
"f28" => 57391,
"f29" => 57392,
"f30" => 57393,
"f31" => 57394,
"f32" => 57395,
"f33" => 57396,
"f34" => 57397,
"f35" => 57398,
// For single printable characters, use the platform-provided base key
// (without any modifiers) to get the correct Unicode codepoint.
// Falls back to lowercasing ASCII letters when platform info isn't available.
key if key.chars().count() == 1 => {
if let Some(base) = key_without_modifiers.and_then(|k| k.chars().next()) {
// Platform provided the unmodified key (e.g., '1' for Shift+1 on US layout)
// CapsLock can cause key_without_modifiers to still report an
// uppercase letter, so normalise to lowercase for the key code.
let base = base.to_ascii_lowercase();
base as u32
} else {
// No platform info available (e.g., tests, WASM). Lowercase ASCII letters
// since that mapping is universal, but use the key as-is for symbols.
let c = key.chars().next()?;
if c.is_ascii_uppercase() {
c.to_ascii_lowercase() as u32
} else {
c as u32
}
}
}
// Unsupported keys
_ => return None,
};
// Build the key code portion: `key_code[:shifted_key]`
// Per spec: "the shifted key must be present only if shift is also present in the modifiers"
let alternate_key_code = if report_alternate && keystroke.shift {
original_char.and_then(|c| {
let shifted = c as u32;
// Only include alternate if it differs from the base key code.
if shifted != key_code {
Some(shifted)
} else {
None
}
})
} else {
None
};
let key_part = match alternate_key_code {
Some(alt) => format!("{key_code}:{alt}"),
None => key_code.to_string(),
};
// Calculate modifier value per the Kitty protocol.
// Kitty modifier bits: shift=1, alt=2, ctrl=4, super=8, hyper=16, meta=32
// The wire value is 1 + (sum of active modifier bits).
//
// Keystroke field mapping:
// keystroke.alt → Alt bit (2): raw Option key on macOS, Alt on other platforms
// keystroke.meta → Alt bit (2): "Option-as-Meta" on macOS (terminal Alt)
// keystroke.cmd → Super bit (8): Cmd on macOS, Super/Win on other platforms
//
// Both `alt` and `meta` map to the Kitty Alt bit because they both represent
// the terminal concept of Alt — `meta` is just the macOS user preference that
// remaps Option to behave as a terminal Meta/Alt key. They cannot both be true
// simultaneously in practice (Option is either raw or Meta, never both).
let mut modifiers = 1u32;
if keystroke.shift {
modifiers += 1;
}
if keystroke.alt || keystroke.meta {
modifiers += 2;
}
if keystroke.ctrl {
modifiers += 4;
}
if keystroke.cmd {
modifiers += 8;
}
// Compute associated text if REPORT_ASSOCIATED_TEXT is active.
// Per spec: "The associated text must not contain control codes (control codes are code
// points below U+0020 and codepoints in the C0 and C1 blocks)."
let associated_text: Option<String> = if report_text {
chars
.filter(|text| !text.is_empty() && !text.chars().any(|c| c.is_control()))
.map(|text| {
text.chars()
.map(|c| (c as u32).to_string())
.collect::<Vec<_>>()
.join(":")
})
} else {
None
};
// Per the Kitty spec, event type 1 (press) is the default and can be omitted.
// We only handle press events for regular keystrokes here; repeat/release events
// are not yet plumbed through KeystrokeWithDetails. Modifier key press/release
// events are handled separately in modifier_key_to_csi_u.
let modifier_part = modifiers.to_string();
// Build the full sequence: CSI key_part [; modifiers [; text]] u
// When associated text is present, the modifiers field must always be included
// (even if it's 1) so the text field is correctly positioned.
let sequence = if let Some(text) = &associated_text {
format!("\x1b[{key_part};{modifier_part};{text}u")
} else if modifiers > 1 {
format!("\x1b[{key_part};{modifier_part}u")
} else {
format!("\x1b[{key_part}u")
};
log::debug!(
"Generated CSI u sequence for key '{}': {}",
keystroke.key,
sequence.escape_default()
);
Some(sequence.into_bytes())
}
/// Encodes a modifier key press/release to a CSI u escape sequence for the Kitty keyboard protocol.
///
/// This is used when REPORT_ALL_KEYS_AS_ESC mode is active to report standalone modifier key
/// press and release events. The format follows the Kitty protocol specification:
/// - Format: CSI <key_code> ; <modifiers> [: <event_type>] u
/// - Modifier key codes: ShiftLeft=57441, ControlLeft=57442, AltLeft=57443, SuperLeft=57444,
/// ShiftRight=57447, ControlRight=57448, AltRight=57449, SuperRight=57450
/// - Event types: 1=press, 2=repeat, 3=release
///
/// Returns None if the key is not a modifier key.
pub fn modifier_key_to_csi_u(
key_code: &KeyCode,
is_press: bool,
report_event_types: bool,
) -> Option<Vec<u8>> {
let kitty_key_code = match key_code {
KeyCode::ShiftLeft => 57441,
KeyCode::ControlLeft => 57442,
KeyCode::AltLeft => 57443,
KeyCode::SuperLeft => 57444,
KeyCode::ShiftRight => 57447,
KeyCode::ControlRight => 57448,
KeyCode::AltRight => 57449,
KeyCode::SuperRight => 57450,
KeyCode::CapsLock => 57358,
KeyCode::NumLock => 57360,
_ => return None,
};
// Per the Kitty spec, when a modifier key is pressed alone, its own modifier
// bit must be included (the "self" bit). For example, pressing Left Shift
// produces modifiers = 1 + 1 = 2 (base 1 + shift bit 1).
let modifiers = 1u32
+ match key_code {
KeyCode::ShiftLeft | KeyCode::ShiftRight => 1,
KeyCode::AltLeft | KeyCode::AltRight => 2,
KeyCode::ControlLeft | KeyCode::ControlRight => 4,
KeyCode::SuperLeft | KeyCode::SuperRight => 8,
_ => 0,
};
let sequence = if report_event_types {
// Event type: 1 = press, 3 = release
let event_type = if is_press { 1 } else { 3 };
// With event types, we need the modifier field for the colon syntax
format!("\x1b[{};{}:{}u", kitty_key_code, modifiers, event_type)
} else {
// Without event type reporting, only report press events
if !is_press {
return None;
}
// Modifiers always > 1 for modifier keys due to the self-bit
format!("\x1b[{};{}u", kitty_key_code, modifiers)
};
log::debug!(
"Generated CSI u sequence for modifier key {:?}: {}",
key_code,
sequence.escape_default()
);
Some(sequence.into_bytes())
}
/// Returns a CSI u escape sequence for a modifier key event if the terminal mode requires it.
///
/// Checks whether the REPORT_ALL_KEYS_AS_ESCAPE flag is active (which means standalone
/// modifier key presses should be reported) and, if so, encodes the modifier key event.
/// Returns `None` if the mode is not active or the key is not a modifier key.
pub fn maybe_kitty_keyboard_escape_sequence(
mode_provider: &dyn ModeProvider,
key_code: &KeyCode,
is_press: bool,
) -> Option<Vec<u8>> {
if !mode_provider.is_term_mode_set(TermMode::KEYBOARD_REPORT_ALL_AS_ESCAPE) {
return None;
}
let report_event_types = mode_provider.is_term_mode_set(TermMode::KEYBOARD_REPORT_EVENT_TYPES);
modifier_key_to_csi_u(key_code, is_press, report_event_types)
}