T44: herdr's shortcuts work, which a user found dead: under kitty's flags a key Shift makes a symbol of goes as its own key with Shift, the symbol its alternate, as Ghostty sends it, read from the layout gpui reads on macOS and Windows; on Windows keys that type no text go as win32-input-mode records, which ConPTY asks for and herdr reads, as Windows Terminal sends them; on Linux gpui keeps no key code, which the docs say; shot learns wait, n and ]
CI / platforms (windows-latest) (push) Failing after 19s
CI / macos (push) Failing after 2m56s
CI / platforms (ubuntu-24.04) (push) Successful in 6m41s

This commit is contained in:
ZacharyZhang-NY committed 2026-10-18 04:00:24 -04:00
1 parent 8d7e730a78
commit fc10c4d491
19 files changed
+548 -118

No files matched your search

+40 -6
View File
@@ -93,7 +93,7 @@ impl TerminalView {
return;
}
self.held.insert(event.keystroke.key.clone());
let facts = self.facts();
let facts = self.facts(&event.keystroke);
let action = if event.is_held || facts.repeat {
Action::Repeat
} else {
@@ -109,7 +109,7 @@ impl TerminalView {
if !self.held.remove(&event.keystroke.key) {
return;
}
let facts = self.facts();
let facts = self.facts(&event.keystroke);
if self.send(&event.keystroke, Action::Release, facts, cx) {
cx.stop_propagation();
}
@@ -138,6 +138,7 @@ impl TerminalView {
alt,
platform: modifiers.platform,
action,
unshifted: facts.unshifted,
};
let mode = *self.pty.term.lock().mode();
let kitty = Kitty {
@@ -147,6 +148,22 @@ impl TerminalView {
all: mode.contains(TermMode::REPORT_ALL_KEYS_AS_ESC),
text: mode.contains(TermMode::REPORT_ASSOCIATED_TEXT),
};
// As in Windows Terminal: kitty's flags first, then Windows' records; text stays text.
#[cfg(windows)]
if self.win32 && kitty == Kitty::default() && !keys::types(&key) {
match super::win32::record(keystroke, action) {
Some(bytes) => {
log::trace!("terminal {}: {key:?} sends {bytes:?}", self.pty.pid);
self.input(bytes, cx);
return true;
}
None => log::debug!(
"terminal {}: no virtual key for {:?}, so it goes as bytes",
self.pty.pid,
keystroke.key
),
}
}
let Some(bytes) = keys::encode(&key, mode.contains(TermMode::APP_CURSOR), kitty) else {
return false;
};
@@ -157,19 +174,35 @@ impl TerminalView {
/// AppKit's facts of the key; logged and none when unreadable.
#[cfg(target_os = "macos")]
fn facts(&self) -> Facts {
super::mac::key_facts().unwrap_or_else(|error| {
fn facts(&self, keystroke: &Keystroke) -> Facts {
super::mac::key_facts(&keystroke.key, keystroke.modifiers.shift).unwrap_or_else(|error| {
log::error!("terminal {}: {error:#}", self.pty.pid);
Facts {
left_option: false,
right_option: false,
repeat: false,
unshifted: None,
}
})
}
#[cfg(not(target_os = "macos"))]
fn facts(&self) -> Facts {
/// Windows' layout names the key gpui named by what Shift made of it.
#[cfg(windows)]
fn facts(&self, keystroke: &Keystroke) -> Facts {
let unshifted = if keystroke.modifiers.shift {
None
} else {
super::win32::unshifted(&keystroke.key)
};
Facts {
repeat: false,
unshifted,
}
}
/// gpui keeps no key code on Linux, so a shifted symbol stays as gpui names it.
#[cfg(not(any(target_os = "macos", windows)))]
fn facts(&self, _: &Keystroke) -> Facts {
Facts::default()
}
@@ -199,6 +232,7 @@ type Facts = super::mac::KeyFacts;
#[derive(Clone, Copy, Debug, Default)]
struct Facts {
repeat: bool,
unshifted: Option<char>,
}
impl EntityInputHandler for TerminalView {
+21 -4
View File
@@ -24,6 +24,8 @@ pub struct Key<'a> {
/// Cmd, the Windows key or Super: kitty's super.
pub platform: bool,
pub action: Action,
/// The key's own character, where gpui named the one Shift made and dropped Shift.
pub unshifted: Option<char>,
}
/// The kitty keyboard flags a program set.
@@ -47,7 +49,21 @@ pub fn encode(key: &Key, app_cursor: bool, kitty: Kitty) -> Option<Vec<u8>> {
}
match functional(key.name) {
Some((number, end)) => named(key, number, end, app_cursor, kitty),
None => typed(key, printable(key.name)?, kitty),
None => match key.unshifted {
// kitty names the key itself, with Shift, and what Shift made of it as its alternate.
Some(own) => {
let made = printable(key.name)?;
let mut name = [0; 4];
let key = Key {
name: own.encode_utf8(&mut name),
shift: true,
unshifted: None,
..*key
};
typed(&key, own, Some(made), kitty)
}
None => typed(key, printable(key.name)?, None, kitty),
},
}
}
@@ -79,7 +95,7 @@ fn printable(name: &str) -> Option<char> {
}
/// Whether the key types its text: some, none of it control, and no modifier that stops text.
fn types(key: &Key) -> bool {
pub(super) fn types(key: &Key) -> bool {
let control = key.text.chars().any(char::is_control);
!key.text.is_empty() && !control && !key.ctrl && !key.alt && !key.platform
}
@@ -165,8 +181,8 @@ fn named(key: &Key, number: u32, end: char, app_cursor: bool, kitty: Kitty) -> O
Some(sequence.into_bytes())
}
/// A printable key: text, then legacy bytes, then CSI u once a flag has more to say.
fn typed(key: &Key, ch: char, kitty: Kitty) -> Option<Vec<u8>> {
/// A printable key: text, then legacy bytes, then CSI u once a flag has more to say; `made` is what Shift made of it.
fn typed(key: &Key, ch: char, made: Option<char>, kitty: Kitty) -> Option<Vec<u8>> {
let typing = types(key);
if typing && !kitty.all && key.action != Action::Release {
return None;
@@ -176,6 +192,7 @@ fn typed(key: &Key, ch: char, kitty: Kitty) -> Option<Vec<u8>> {
.chars()
.next()
.filter(|&typed| typing && typed != ch && !typed.is_control())
.or(made.filter(|&made| made != ch))
.or_else(|| (key.shift && ch.is_ascii_lowercase()).then(|| ch.to_ascii_uppercase()))
.filter(|_| key.shift && kitty.alternates);
let event = event(key, kitty);
+56
View File
@@ -12,6 +12,7 @@ fn key(spec: &'static str, text: &'static str) -> Key<'static> {
alt: parts.contains(&"alt"),
platform: parts.contains(&"cmd"),
action: Action::Press,
unshifted: None,
}
}
@@ -296,6 +297,61 @@ fn flags_that_only_add_fields_keep_legacy_keys() {
);
}
#[test]
fn kitty_reports_the_key_shift_turned_into_a_symbol() {
// gpui names Shift-[ by its `{` and drops Shift; the platform gives back `[`.
let made = |spec, text, action| Key {
unshifted: Some('['),
action,
..key(spec, text)
};
let herdr = Kitty {
events: true,
alternates: true,
..DISAMBIGUATE
};
assert_eq!(
kitty(made("cmd+ctrl+{", "", Action::Press), herdr).as_deref(),
Some("\x1b[91:123;14u")
);
assert_eq!(
kitty(made("cmd+ctrl+{", "", Action::Release), herdr).as_deref(),
Some("\x1b[91:123;14:3u")
);
assert_eq!(
kitty(made("ctrl+{", "", Action::Press), DISAMBIGUATE).as_deref(),
Some("\x1b[91;6u")
);
assert_eq!(
kitty(made("{", "{", Action::Press), herdr),
None,
"it types"
);
let all = Kitty {
all: true,
alternates: true,
..DISAMBIGUATE
};
assert_eq!(
kitty(made("{", "{", Action::Press), all).as_deref(),
Some("\x1b[91:123;2u")
);
assert_eq!(
legacy(made("ctrl+{", "", Action::Press), false).as_deref(),
Some("\x1b[123;5u"),
"legacy keeps fixterms' shifted character"
);
let same = Key {
unshifted: Some('7'),
..key("ctrl+7", "")
};
assert_eq!(
kitty(same, herdr).as_deref(),
Some("\x1b[55;6u"),
"no alternate where Shift made the same character"
);
}
#[test]
fn super_is_kittys_eighth_bit() {
let events = Kitty {
+107 -9
View File
@@ -1,3 +1,6 @@
use std::ffi::c_void;
use anyhow::Context as _;
use block::ConcreteBlock;
use objc::{
class, msg_send,
@@ -5,13 +8,43 @@ use objc::{
sel, sel_impl,
};
#[link(name = "Carbon", kind = "framework")]
unsafe extern "C" {
static kTISPropertyUnicodeKeyLayoutData: *const c_void;
fn TISCopyCurrentKeyboardLayoutInputSource() -> *mut c_void;
fn TISGetInputSourceProperty(source: *mut c_void, key: *const c_void) -> *const c_void;
fn LMGetKbdType() -> u16;
fn UCKeyTranslate(
layout: *const c_void,
code: u16,
action: u16,
modifiers: u32,
keyboard: u32,
options: u32,
dead: *mut u32,
most: usize,
length: *mut usize,
text: *mut u16,
) -> u32;
}
#[link(name = "CoreFoundation", kind = "framework")]
unsafe extern "C" {
fn CFDataGetBytePtr(data: *const c_void) -> *const u8;
fn CFRelease(object: *const c_void);
}
/// AppKit's event types for a key going down and coming up.
const KEY_DOWN: u64 = 10;
const KEY_UP: u64 = 11;
/// The Command key's bit in an event's modifier flags.
/// The Shift and Command keys' bits in an event's modifier flags.
const SHIFT: u64 = 1 << 17;
const COMMAND: u64 = 1 << 20;
/// Command in a layout's modifier state, as `UCKeyTranslate` takes it.
const LAYOUT_COMMAND: u32 = 1;
/// The device bits of the left and right Option keys in an event's modifier flags.
const LEFT_OPTION: u64 = 0x20;
const RIGHT_OPTION: u64 = 0x40;
@@ -23,11 +56,13 @@ pub struct KeyFacts {
pub right_option: bool,
/// Held down; gpui loses this for keys AppKit routes through commands.
pub repeat: bool,
/// The key's own character, where gpui named the one Shift made and dropped Shift.
pub unshifted: Option<char>,
}
/// The facts of the key event AppKit is handling now.
pub fn key_facts() -> anyhow::Result<KeyFacts> {
unsafe {
/// The facts of the key event AppKit is handling now, which gpui named `name`, with `shift` or not.
pub fn key_facts(name: &str, shift: bool) -> anyhow::Result<KeyFacts> {
let (flags, repeat, code) = unsafe {
let app: *mut Object = msg_send![class!(NSApplication), sharedApplication];
let event: *mut Object = msg_send![app, currentEvent];
anyhow::ensure!(!event.is_null(), "macOS: no key event to read");
@@ -38,12 +73,75 @@ pub fn key_facts() -> anyhow::Result<KeyFacts> {
);
let flags: u64 = msg_send![event, modifierFlags];
let repeat: BOOL = msg_send![event, isARepeat];
Ok(KeyFacts {
left_option: flags & LEFT_OPTION != 0,
right_option: flags & RIGHT_OPTION != 0,
repeat: repeat != NO,
})
let code: u16 = msg_send![event, keyCode];
(flags, repeat != NO, code)
};
let one = name.chars().count() == 1;
let unshifted = if flags & SHIFT != 0 && !shift && one {
let own = own(code, flags)?;
log::debug!("macOS: Shift made {name:?} of key {code}, whose own is {own:?}");
own
} else {
None
};
Ok(KeyFacts {
left_option: flags & LEFT_OPTION != 0,
right_option: flags & RIGHT_OPTION != 0,
repeat,
unshifted,
})
}
/// Key `code`'s character without Shift, read as gpui reads key names: Command's layout while Command is down or the plain one types no ASCII.
fn own(code: u16, flags: u64) -> anyhow::Result<Option<char>> {
let command =
flags & COMMAND != 0 || (!typed(0, 0)?.is_ascii() && typed(0, LAYOUT_COMMAND)?.is_ascii());
let own = typed(code, if command { LAYOUT_COMMAND } else { 0 })?;
let mut chars = own.chars();
Ok(match (chars.next(), chars.next()) {
(Some(ch), None) if !ch.is_control() => Some(ch),
_ => None,
})
}
/// What key `code` types under `modifiers` in the current layout; a dead key types its own mark.
fn typed(code: u16, modifiers: u32) -> anyhow::Result<String> {
const SPACE: u16 = 49;
let mut text = [0u16; 4];
let mut length = 0;
unsafe {
let source = TISCopyCurrentKeyboardLayoutInputSource();
anyhow::ensure!(!source.is_null(), "macOS: no keyboard layout");
let data = TISGetInputSourceProperty(source, kTISPropertyUnicodeKeyLayoutData);
if !data.is_null() {
let layout = CFDataGetBytePtr(data).cast::<c_void>();
let keyboard = u32::from(LMGetKbdType());
let mut dead = 0;
for key in [code, SPACE] {
UCKeyTranslate(
layout,
key,
0,
modifiers,
keyboard,
0,
&mut dead,
text.len(),
&mut length,
text.as_mut_ptr(),
);
if dead == 0 {
break;
}
}
}
CFRelease(source);
anyhow::ensure!(
!data.is_null(),
"macOS: the keyboard layout has no key table"
);
}
String::from_utf16(&text[..length]).context("macOS: the layout typed no UTF-16")
}
/// Turns off the accent menu a held key opens, so held keys repeat as in other terminals.
+2
View File
@@ -23,6 +23,8 @@ mod titles;
#[cfg(unix)]
mod unix;
mod view;
#[cfg(windows)]
mod win32;
pub mod zoom;
pub use pty::{Events, Launch, Listener, Pty, PtyEvent, config, fit, spawn};
+3
View File
@@ -29,6 +29,9 @@ pub enum PtyEvent {
Title(String),
/// A full reset, `ESC c`.
Reset,
/// win32-input-mode set or reset.
#[cfg(windows)]
Win32(bool),
}
/// Hands the grid's events to the session; `pid` names the program in logs.
+18 -87
View File
@@ -9,7 +9,6 @@ use polling::{Event, PollMode, Poller};
use super::{
marks,
mouse::MouseMode,
notify::{self, Kitty},
prompts::{self, Prompts},
pty::PtyEvent,
@@ -24,7 +23,7 @@ pub(super) const MOST: usize = 4096;
pub(crate) struct Tapped<P> {
pub inner: P,
scanner: Osc,
mouse: MouseModes,
modes: Modes,
prompts: Prompts,
kitty: Kitty,
titles: Titles,
@@ -38,7 +37,7 @@ impl<P> Tapped<P> {
Self {
inner,
scanner: Osc::default(),
mouse: MouseModes::default(),
modes: Modes::default(),
prompts: Prompts::default(),
kitty: Kitty::default(),
titles: Titles::default(),
@@ -66,12 +65,24 @@ impl<P: EventedReadWrite> io::Read for Tapped<P> {
self.scanner.feed(read, &mut |found| {
report(found, events, pid, last, kitty, titles)
});
if let Some(mode) = self.mouse.feed(read) {
#[cfg(windows)]
let win32 = self.modes.win32;
if let Some(mode) = self.modes.feed(read) {
log::info!("pty {pid}: mouse {mode:?}");
if events.unbounded_send(PtyEvent::Mouse(mode)).is_err() {
log::debug!("pty {pid}: a mouse mode after its session closed");
}
}
#[cfg(windows)]
if self.modes.win32 != win32 {
log::info!("pty {pid}: win32-input-mode {}", self.modes.win32);
if events
.unbounded_send(PtyEvent::Win32(self.modes.win32))
.is_err()
{
log::debug!("pty {pid}: win32-input-mode after its session closed");
}
}
let out = self.prompts.pass(&buf[..count]);
buf[..out.len()].copy_from_slice(out);
Ok(out.len())
@@ -338,89 +349,6 @@ impl Osc {
/// The most values, subparameters too, one sequence holds before vte drops it.
const PARAMS: usize = 32;
#[derive(Clone, Copy, Debug, Default, PartialEq)]
enum Csi {
#[default]
Ground,
Escape,
Bracket,
Private,
}
/// The mouse modes as xterm holds them, read as vte reads; alacritty drops 9.
#[derive(Default)]
pub(crate) struct MouseModes {
state: Csi,
params: Vec<u32>,
/// Values begun, subparameters too.
fields: usize,
/// Past a `:`, whose subparameters the parser's mode sets skip.
sub: bool,
mode: MouseMode,
}
impl MouseModes {
/// The mode after `bytes`, when they changed it.
pub fn feed(&mut self, bytes: &[u8]) -> Option<MouseMode> {
let was = self.mode;
let mut rest = bytes;
while !rest.is_empty() {
if self.state == Csi::Ground {
let Some(at) = rest.iter().position(|&byte| byte == 0x1b) else {
break;
};
self.state = Csi::Escape;
rest = &rest[at + 1..];
continue;
}
let byte = rest[0];
rest = &rest[1..];
self.state = match (self.state, byte) {
(_, 0x18 | 0x1a) => Csi::Ground,
(_, 0x1b) => Csi::Escape,
// Mid-sequence, C0 runs and DEL is skipped, as in vte.
(state, 0x00..=0x1f | 0x7f..=0xff) => state,
(Csi::Escape, b'[') => Csi::Bracket,
(Csi::Escape, b'c') => {
self.mode = MouseMode::default();
Csi::Ground
}
(Csi::Bracket, b'?') => {
self.params = vec![0];
self.fields = 1;
self.sub = false;
Csi::Private
}
(Csi::Private, b'0'..=b'9') => {
if !self.sub {
let last = self.params.last_mut().expect("a parameter in progress");
*last = last
.saturating_mul(10)
.saturating_add(u32::from(byte - b'0'));
}
Csi::Private
}
(Csi::Private, b':' | b';') if self.fields < PARAMS => {
self.fields += 1;
self.sub = byte == b':';
if !self.sub {
self.params.push(0);
}
Csi::Private
}
(Csi::Private, b'h' | b'l') => {
for &mode in &self.params {
self.mode.apply(mode, byte == b'h');
}
Csi::Ground
}
_ => Csi::Ground,
};
}
(self.mode != was).then_some(self.mode)
}
}
/// The local folder an OSC 7 `file://host/path` names; another host's folder is none.
pub(crate) fn folder(body: &[u8]) -> Option<PathBuf> {
let Some(rest) = body.strip_prefix(b"file://") else {
@@ -480,5 +408,8 @@ fn decode(bytes: &[u8]) -> Option<String> {
String::from_utf8(out).ok()
}
mod modes;
#[cfg(test)]
mod tests;
pub(crate) use modes::Modes;
+98
View File
@@ -0,0 +1,98 @@
//! The private modes the tap follows before the parser.
use super::PARAMS;
use crate::term::mouse::MouseMode;
#[derive(Clone, Copy, Debug, Default, PartialEq)]
enum Csi {
#[default]
Ground,
Escape,
Bracket,
Private,
}
/// The mouse modes as xterm holds them, read as vte reads; alacritty drops 9. On Windows, win32-input-mode too.
#[derive(Default)]
pub(crate) struct Modes {
state: Csi,
params: Vec<u32>,
/// Values begun, subparameters too.
fields: usize,
/// Past a `:`, whose subparameters the parser's mode sets skip.
sub: bool,
pub(super) mode: MouseMode,
/// Keys go as Windows' records, as ConPTY asks; a full reset ends it, as in Windows Terminal.
#[cfg(windows)]
pub(super) win32: bool,
}
impl Modes {
/// The mode after `bytes`, when they changed it.
pub fn feed(&mut self, bytes: &[u8]) -> Option<MouseMode> {
let was = self.mode;
let mut rest = bytes;
while !rest.is_empty() {
if self.state == Csi::Ground {
let Some(at) = rest.iter().position(|&byte| byte == 0x1b) else {
break;
};
self.state = Csi::Escape;
rest = &rest[at + 1..];
continue;
}
let byte = rest[0];
rest = &rest[1..];
self.state = match (self.state, byte) {
(_, 0x18 | 0x1a) => Csi::Ground,
(_, 0x1b) => Csi::Escape,
// Mid-sequence, C0 runs and DEL is skipped, as in vte.
(state, 0x00..=0x1f | 0x7f..=0xff) => state,
(Csi::Escape, b'[') => Csi::Bracket,
(Csi::Escape, b'c') => {
self.mode = MouseMode::default();
#[cfg(windows)]
{
self.win32 = false;
}
Csi::Ground
}
(Csi::Bracket, b'?') => {
self.params = vec![0];
self.fields = 1;
self.sub = false;
Csi::Private
}
(Csi::Private, b'0'..=b'9') => {
if !self.sub {
let last = self.params.last_mut().expect("a parameter in progress");
*last = last
.saturating_mul(10)
.saturating_add(u32::from(byte - b'0'));
}
Csi::Private
}
(Csi::Private, b':' | b';') if self.fields < PARAMS => {
self.fields += 1;
self.sub = byte == b':';
if !self.sub {
self.params.push(0);
}
Csi::Private
}
(Csi::Private, b'h' | b'l') => {
for &mode in &self.params {
self.mode.apply(mode, byte == b'h');
#[cfg(windows)]
if mode == crate::term::win32::MODE {
self.win32 = byte == b'h';
}
}
Csi::Ground
}
_ => Csi::Ground,
};
}
(self.mode != was).then_some(self.mode)
}
}
+16 -3
View File
@@ -1,4 +1,5 @@
use super::*;
use crate::term::mouse::MouseMode;
/// What the scanner finds in `chunks`, each one read: a body, or `RESET`.
fn scan(chunks: &[&[u8]]) -> Vec<String> {
@@ -86,7 +87,7 @@ fn an_osc_99_body_holds_its_metadata_and_a_full_payload() {
#[test]
fn mouse_modes_follow_private_modes_in_order_across_reads() {
use crate::term::mouse::{Format, Tracking};
let mut modes = MouseModes::default();
let mut modes = Modes::default();
assert_eq!(modes.feed(b"a\x1b[?"), None);
assert!(modes.feed(b"1006;9h").is_some());
assert_eq!(modes.mode.format, Format::Sgr);
@@ -106,7 +107,7 @@ fn mouse_modes_follow_private_modes_in_order_across_reads() {
#[test]
fn mouse_modes_read_through_controls_as_the_parser_does() {
use crate::term::mouse::{Format, Tracking};
let mut modes = MouseModes::default();
let mut modes = Modes::default();
modes.feed(b"\x1b[?1000\x00h\x1b[?1006");
modes.feed(b"\x00h");
let on = MouseMode {
@@ -220,7 +221,7 @@ fn the_tap_reads_mouse_modes_as_vte_does_byte_by_byte() {
.chain([values(31), values(32)])
{
let case = case.as_slice();
let mut modes = MouseModes::default();
let mut modes = Modes::default();
for end in 1..=case.len() {
modes.feed(&case[end - 1..end]);
assert_eq!(modes.mode, parsed(&case[..end]), "{case:?} to byte {end}");
@@ -278,3 +279,15 @@ fn a_title_s_body_holds_65536_bytes_and_another_s_4096() {
"another stays at 4096"
);
}
#[cfg(windows)]
#[test]
fn win32_input_mode_follows_conptys_ask_until_a_full_reset() {
let mut modes = Modes::default();
modes.feed(b"\x1b[c\x1b[?1004h\x1b[?9001h");
assert!(modes.win32);
modes.feed(b"\x1b[?9001l");
assert!(!modes.win32);
modes.feed(b"\x1b[?9001h\x1bc");
assert!(!modes.win32, "a full reset ends it");
}
+7
View File
@@ -67,6 +67,9 @@ pub struct TerminalView {
pub(super) pointer: Pointer,
/// The mouse mode, as the tap follows it.
pub(super) mouse: MouseMode,
/// Keys go as Windows' records, as ConPTY asks.
#[cfg(windows)]
pub(super) win32: bool,
/// Keys whose press this terminal saw, so their releases may go out.
pub(super) held: HashSet<String>,
pub(super) finder: Option<Finder>,
@@ -130,6 +133,8 @@ impl TerminalView {
caret: Bounds::default(),
pointer: Pointer::default(),
mouse: MouseMode::default(),
#[cfg(windows)]
win32: false,
held: HashSet::new(),
finder: None,
size,
@@ -260,6 +265,8 @@ impl TerminalView {
}
PtyEvent::Term(event) => self.term_event(event, cx),
PtyEvent::Mouse(mode) => self.mouse = mode,
#[cfg(windows)]
PtyEvent::Win32(on) => self.win32 = on,
PtyEvent::Mark(Mark::Done(status)) => self.finished(status, cx),
// A new prompt ends what was working, waiting or idle; done and error stay.
PtyEvent::Mark(Mark::Prompt) => {
+166
View File
@@ -0,0 +1,166 @@
//! win32-input-mode, which ConPTY asks for: a key goes as Windows' own record, as Windows Terminal sends it.
use gpui::Keystroke;
use windows_sys::Win32::{
System::Console::{
CAPSLOCK_ON, ENHANCED_KEY, LEFT_ALT_PRESSED, LEFT_CTRL_PRESSED, NUMLOCK_ON,
RIGHT_ALT_PRESSED, RIGHT_CTRL_PRESSED, SCROLLLOCK_ON, SHIFT_PRESSED,
},
UI::Input::KeyboardAndMouse::{
GetKeyState, GetKeyboardState, MAPVK_VK_TO_CHAR, MAPVK_VK_TO_VSC, MapVirtualKeyW,
ToUnicode, VK_APPS, VK_BACK, VK_BROWSER_BACK, VK_BROWSER_FORWARD, VK_CAPITAL, VK_DELETE,
VK_DOWN, VK_END, VK_ESCAPE, VK_F1, VK_HOME, VK_INSERT, VK_LCONTROL, VK_LEFT, VK_LMENU,
VK_NEXT, VK_NUMLOCK, VK_PRIOR, VK_RCONTROL, VK_RETURN, VK_RIGHT, VK_RMENU, VK_SCROLL,
VK_SHIFT, VK_SPACE, VK_TAB, VK_UP, VkKeyScanW,
},
};
use super::keys::Action;
/// The mode ConPTY sets, `CSI ? 9001 h`.
pub const MODE: u32 = 9001;
/// `keystroke` as ConPTY reads it, `CSI Vk ; Sc ; Uc ; Kd ; Cs ; Rc _`; none for a key no virtual key names.
pub fn record(keystroke: &Keystroke, action: Action) -> Option<Vec<u8>> {
let (key, enhanced) = virtual_key(&keystroke.key)?;
let scan = unsafe { MapVirtualKeyW(u32::from(key), MAPVK_VK_TO_VSC) };
let mut state = [0u8; 256];
let mut unit = [0u16; 2];
// Flag 4 leaves the keyboard's dead key as it is.
let typed = unsafe {
if GetKeyboardState(state.as_mut_ptr()) == 0 {
log::error!("windows: the keyboard state did not read");
return None;
}
ToUnicode(
u32::from(key),
scan,
state.as_ptr(),
unit.as_mut_ptr(),
unit.len() as i32,
4,
)
};
let unit = if typed == 1 { unit[0] } else { 0 };
let down = u8::from(action != Action::Release);
let control = control(enhanced);
Some(format!("\x1b[{key};{scan};{unit};{down};{control};1_").into_bytes())
}
/// The key's own character, where gpui named `made`, what Shift made of it, and dropped Shift.
pub fn unshifted(made: &str) -> Option<char> {
let mut chars = made.chars();
let (Some(made), None) = (chars.next(), chars.next()) else {
return None;
};
if unsafe { GetKeyState(i32::from(VK_SHIFT)) } >= 0 {
return None;
}
let mut buffer = [0u16; 2];
let &mut [unit] = made.encode_utf16(&mut buffer) else {
return None;
};
// The high byte names the modifiers the character takes: here Shift alone.
let scan = unsafe { VkKeyScanW(unit) } as u16;
if scan == u16::MAX || scan >> 8 != 1 {
return None;
}
let own = unsafe { MapVirtualKeyW(u32::from(scan & 0xFF), MAPVK_VK_TO_CHAR) } & 0xFFFF;
char::from_u32(own)
.map(|own| own.to_ascii_lowercase())
.filter(|own| !own.is_control())
}
/// The virtual key gpui's `name` stands for, and whether it sits in the enhanced block.
fn virtual_key(name: &str) -> Option<(u16, bool)> {
let named = match name {
"enter" => (VK_RETURN, false),
"tab" => (VK_TAB, false),
"escape" => (VK_ESCAPE, false),
"backspace" => (VK_BACK, false),
"space" => (VK_SPACE, false),
"menu" => (VK_APPS, true),
"back" => (VK_BROWSER_BACK, true),
"forward" => (VK_BROWSER_FORWARD, true),
"left" => (VK_LEFT, true),
"up" => (VK_UP, true),
"right" => (VK_RIGHT, true),
"down" => (VK_DOWN, true),
"home" => (VK_HOME, true),
"end" => (VK_END, true),
"pageup" => (VK_PRIOR, true),
"pagedown" => (VK_NEXT, true),
"insert" => (VK_INSERT, true),
"delete" => (VK_DELETE, true),
name => {
if let Some(number) = name
.strip_prefix('f')
.and_then(|number| number.parse::<u16>().ok())
.filter(|number| (1..=24).contains(number))
{
return Some((VK_F1 + number - 1, false));
}
let mut chars = name.chars();
let (Some(ch), None) = (chars.next(), chars.next()) else {
return None;
};
let mut buffer = [0u16; 2];
let &mut [unit] = ch.encode_utf16(&mut buffer) else {
return None;
};
let scan = unsafe { VkKeyScanW(unit) } as u16;
return (scan != u16::MAX).then_some((scan & 0xFF, false));
}
};
Some(named)
}
/// Windows' control key state now: the modifier keys, the locks, and the enhanced block.
fn control(enhanced: bool) -> u32 {
let down = |key: u16| unsafe { GetKeyState(i32::from(key)) } < 0;
let on = |key: u16| unsafe { GetKeyState(i32::from(key)) } & 1 != 0;
[
(down(VK_SHIFT), SHIFT_PRESSED),
(down(VK_LCONTROL), LEFT_CTRL_PRESSED),
(down(VK_RCONTROL), RIGHT_CTRL_PRESSED),
(down(VK_LMENU), LEFT_ALT_PRESSED),
(down(VK_RMENU), RIGHT_ALT_PRESSED),
(on(VK_CAPITAL), CAPSLOCK_ON),
(on(VK_NUMLOCK), NUMLOCK_ON),
(on(VK_SCROLL), SCROLLLOCK_ON),
(enhanced, ENHANCED_KEY),
]
.into_iter()
.filter(|(held, _)| *held)
.fold(0, |state, (_, flag)| state | flag)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn named_keys_and_letters_find_their_virtual_keys() {
assert_eq!(virtual_key("enter"), Some((VK_RETURN, false)));
assert_eq!(virtual_key("left"), Some((VK_LEFT, true)));
assert_eq!(virtual_key("f12"), Some((VK_F1 + 11, false)));
assert_eq!(virtual_key("f25"), None);
assert_eq!(virtual_key("a"), Some((u16::from(b'A'), false)));
}
#[test]
fn a_record_names_the_key_its_scan_code_and_its_press() {
let key = Keystroke::parse("enter").expect("a keystroke");
let text =
|action| String::from_utf8(record(&key, action).expect("a record")).expect("text");
let scan = unsafe { MapVirtualKeyW(u32::from(VK_RETURN), MAPVK_VK_TO_VSC) };
let down = text(Action::Press);
assert!(
down.starts_with(&format!("\x1b[13;{scan};13;1;")),
"{down:?}"
);
assert!(down.ends_with(";1_"), "{down:?}");
let up = text(Action::Release);
assert!(up.starts_with(&format!("\x1b[13;{scan};13;0;")), "{up:?}");
}
}