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 ]
This commit is contained in:
19 files changed
+548
-118
No files matched your search
+40
-6
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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
@@ -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;
|
||||
@@ -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
@@ -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");
|
||||
}
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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:?}");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user