Files
ELYTerminal/src/term/input.rs
T

287 lines
8.5 KiB
Rust

use std::ops::Range;
use alacritty_terminal::{grid::Scroll, term::TermMode};
use ely_gpui_component::forms::{Copy, Paste, SelectAll};
use gpui::{
App, Bounds, Context, EntityInputHandler, KeyBinding, KeyDownEvent, KeyUpEvent, Keystroke,
NoAction, Pixels, Point, UTF16Selection, Window, actions,
};
use super::{
keys::{self, Action, Key, Kitty},
view::TerminalView,
};
actions!(elyterminal, [Find, FindNewer, PreviousPrompt, NextPrompt]);
/// The key context a terminal's root sets.
pub const CONTEXT: &str = "Terminal";
/// The key context of a terminal's find box.
pub const FIND: &str = "TerminalFind";
/// The terminal's keys; Ely's focus keys reach its listeners.
pub fn bind_keys(cx: &mut App) {
let (copy, paste, all, find) = if cfg!(target_os = "macos") {
("cmd-c", "cmd-v", "cmd-a", "cmd-f")
} else {
(
"ctrl-shift-c",
"ctrl-shift-v",
"ctrl-shift-a",
"ctrl-shift-f",
)
};
// Ghostty's jumps: Cmd with or without Shift on macOS, Ctrl-Shift elsewhere.
let (up, down): (&[&str], &[&str]) = if cfg!(target_os = "macos") {
(&["cmd-up", "cmd-shift-up"], &["cmd-down", "cmd-shift-down"])
} else {
(&["ctrl-shift-up"], &["ctrl-shift-down"])
};
cx.bind_keys(
up.iter()
.map(|key| KeyBinding::new(key, PreviousPrompt, Some(CONTEXT))),
);
cx.bind_keys(
down.iter()
.map(|key| KeyBinding::new(key, NextPrompt, Some(CONTEXT))),
);
cx.bind_keys([
KeyBinding::new(copy, Copy, Some(CONTEXT)),
KeyBinding::new(paste, Paste, Some(CONTEXT)),
KeyBinding::new(all, SelectAll, Some(CONTEXT)),
KeyBinding::new(find, Find, Some(CONTEXT)),
KeyBinding::new(
"shift-enter",
FindNewer,
Some(&format!("{FIND} > ElyInput")),
),
]);
cx.bind_keys(
["tab", "shift-tab", "f6", "shift-f6"]
.map(|key| KeyBinding::new(key, NoAction, Some(CONTEXT))),
);
// Windows pastes on Ctrl-V too, as Windows Terminal does.
if cfg!(windows) {
cx.bind_keys([KeyBinding::new("ctrl-v", Paste, Some(CONTEXT))]);
}
}
impl TerminalView {
/// Bytes from the keyboard: back to the newest line, no selection, then to the program.
pub(super) fn input(&mut self, bytes: Vec<u8>, cx: &mut Context<Self>) {
let mut term = self.pty.term.lock();
term.scroll_display(Scroll::Bottom);
if term.selection.take().is_some() {
log::debug!("terminal {}: input clears the selection", self.pty.pid);
}
drop(term);
self.pty.write(bytes);
cx.notify();
}
pub(super) fn key_down(
&mut self,
event: &KeyDownEvent,
_: &mut Window,
cx: &mut Context<Self>,
) {
if event.prefer_character_input {
return;
}
self.held.insert(event.keystroke.key.clone());
let facts = self.facts();
let action = if event.is_held || facts.repeat {
Action::Repeat
} else {
Action::Press
};
if self.send(&event.keystroke, action, facts, cx) {
cx.stop_propagation();
}
}
/// A release goes out only for a press this terminal saw: none for one a binding took.
pub(super) fn key_up(&mut self, event: &KeyUpEvent, _: &mut Window, cx: &mut Context<Self>) {
if !self.held.remove(&event.keystroke.key) {
return;
}
let facts = self.facts();
if self.send(&event.keystroke, Action::Release, facts, cx) {
cx.stop_propagation();
}
}
/// Sends what `keystroke` means to the program; false when it is text the input method types.
fn send(
&mut self,
keystroke: &Keystroke,
action: Action,
facts: Facts,
cx: &mut Context<Self>,
) -> bool {
let modifiers = keystroke.modifiers;
let alt = modifiers.alt && Self::alt_is_meta(facts, cx);
let text = match &keystroke.key_char {
Some(_) if alt && cfg!(target_os = "macos") => "",
Some(text) => text.as_str(),
None => "",
};
let key = Key {
name: &keystroke.key,
text,
shift: modifiers.shift,
ctrl: modifiers.control,
alt,
platform: modifiers.platform,
action,
};
let mode = *self.pty.term.lock().mode();
let kitty = Kitty {
disambiguate: mode.contains(TermMode::DISAMBIGUATE_ESC_CODES),
events: mode.contains(TermMode::REPORT_EVENT_TYPES),
alternates: mode.contains(TermMode::REPORT_ALTERNATE_KEYS),
all: mode.contains(TermMode::REPORT_ALL_KEYS_AS_ESC),
text: mode.contains(TermMode::REPORT_ASSOCIATED_TEXT),
};
let Some(bytes) = keys::encode(&key, mode.contains(TermMode::APP_CURSOR), kitty) else {
return false;
};
log::trace!("terminal {}: {key:?} sends {bytes:?}", self.pty.pid);
self.input(bytes, cx);
true
}
/// 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| {
log::error!("terminal {}: {error:#}", self.pty.pid);
Facts {
left_option: false,
right_option: false,
repeat: false,
}
})
}
#[cfg(not(target_os = "macos"))]
fn facts(&self) -> Facts {
Facts::default()
}
/// Whether a held Alt is a modifier; on macOS only the Option keys the settings name.
#[cfg(target_os = "macos")]
fn alt_is_meta(facts: Facts, cx: &App) -> bool {
use crate::settings::{OptionAsAlt, SettingsStore};
match SettingsStore::get(cx).option_as_alt {
OptionAsAlt::Off => false,
OptionAsAlt::Both => true,
OptionAsAlt::Left => facts.left_option,
OptionAsAlt::Right => facts.right_option,
}
}
#[cfg(not(target_os = "macos"))]
fn alt_is_meta(_: Facts, _: &App) -> bool {
true
}
}
#[cfg(target_os = "macos")]
type Facts = super::mac::KeyFacts;
/// Other platforms tell gpui all it needs.
#[cfg(not(target_os = "macos"))]
#[derive(Clone, Copy, Debug, Default)]
struct Facts {
repeat: bool,
}
impl EntityInputHandler for TerminalView {
fn text_for_range(
&mut self,
_: Range<usize>,
_: &mut Option<Range<usize>>,
_: &mut Window,
_: &mut Context<Self>,
) -> Option<String> {
None
}
fn selected_text_range(
&mut self,
_: bool,
_: &mut Window,
_: &mut Context<Self>,
) -> Option<UTF16Selection> {
let end = self
.marked
.as_ref()
.map_or(0, |marked| marked.encode_utf16().count());
Some(UTF16Selection {
range: end..end,
reversed: false,
})
}
fn marked_text_range(&self, _: &mut Window, _: &mut Context<Self>) -> Option<Range<usize>> {
self.marked
.as_ref()
.map(|marked| 0..marked.encode_utf16().count())
}
fn unmark_text(&mut self, _: &mut Window, cx: &mut Context<Self>) {
log::debug!("terminal {}: composing ends", self.pty.pid);
self.marked = None;
cx.notify();
}
fn replace_text_in_range(
&mut self,
_: Option<Range<usize>>,
text: &str,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.marked = None;
if !text.is_empty() {
log::trace!("terminal {}: typed {text:?}", self.pty.pid);
self.input(text.as_bytes().to_vec(), cx);
}
cx.notify();
}
fn replace_and_mark_text_in_range(
&mut self,
_: Option<Range<usize>>,
text: &str,
_: Option<Range<usize>>,
_: &mut Window,
cx: &mut Context<Self>,
) {
log::debug!("terminal {}: composing {text:?}", self.pty.pid);
self.marked = (!text.is_empty()).then(|| text.to_string());
cx.notify();
}
fn bounds_for_range(
&mut self,
_: Range<usize>,
_: Bounds<Pixels>,
_: &mut Window,
_: &mut Context<Self>,
) -> Option<Bounds<Pixels>> {
Some(self.caret)
}
fn character_index_for_point(
&mut self,
_: Point<Pixels>,
_: &mut Window,
_: &mut Context<Self>,
) -> Option<usize> {
None
}
}