T08: keys: xterm and kitty encoding, Option as Alt by side, IME composition at the cursor; shot types real AppKit keys
This commit is contained in:
15 files changed
+1949
-28
No files matched your search
+6
-1
@@ -7,9 +7,11 @@ use gpui::{
|
||||
Render, Styled, TitlebarOptions, Window, WindowBounds, WindowHandle, WindowOptions, px, size,
|
||||
};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::term::mac;
|
||||
use crate::{
|
||||
settings::SettingsStore,
|
||||
term::{ICONS, Launch, MONO, TerminalView, config, shell::ShellIntegration, spawn},
|
||||
term::{ICONS, Launch, MONO, TerminalView, config, input, shell::ShellIntegration, spawn},
|
||||
themes,
|
||||
};
|
||||
|
||||
@@ -52,6 +54,9 @@ pub fn start(cx: &mut App) -> anyhow::Result<()> {
|
||||
}
|
||||
SettingsStore::init(cx)?;
|
||||
ShellIntegration::init(cx)?;
|
||||
input::bind_keys(cx);
|
||||
#[cfg(target_os = "macos")]
|
||||
mac::repeat_held_keys();
|
||||
let family = &SettingsStore::get(cx).font_family;
|
||||
anyhow::ensure!(
|
||||
cx.text_system().all_font_names().contains(family),
|
||||
|
||||
+17
-1
@@ -60,6 +60,8 @@ pub struct Cursor {
|
||||
pub struct Frame {
|
||||
pub rows: Vec<Row>,
|
||||
pub cursor: Option<Cursor>,
|
||||
/// The cursor's row and column while on screen, shown or hidden.
|
||||
pub caret: Option<(usize, usize)>,
|
||||
}
|
||||
|
||||
impl Frame {
|
||||
@@ -121,7 +123,15 @@ pub fn frame<T: EventListener>(term: &Term<T>, ink: &Ink) -> Frame {
|
||||
rows[row].alone.push((point.column.0, alone, look));
|
||||
}
|
||||
}
|
||||
Frame { rows, cursor }
|
||||
let row = at.line.0 + offset;
|
||||
let caret = (0..rows.len() as i32)
|
||||
.contains(&row)
|
||||
.then_some((row as usize, at.column.0));
|
||||
Frame {
|
||||
rows,
|
||||
cursor,
|
||||
caret,
|
||||
}
|
||||
}
|
||||
|
||||
fn push(row: &mut Row, shown: char, style: Style) {
|
||||
@@ -272,6 +282,12 @@ mod tests {
|
||||
assert_eq!(frame.rows[0].alone[0].2.fg, ink.fg);
|
||||
assert_eq!(frame.text(), "a\u{2500}\u{E0B0}\u{2500}\u{301}");
|
||||
assert_eq!(frame.cursor.expect("a cursor").text, "\u{E0B0}");
|
||||
let (gone, _) = shown(b"ab\x1b[?25l");
|
||||
assert_eq!(
|
||||
(gone.cursor, gone.caret),
|
||||
(None, Some((0, 2))),
|
||||
"a hidden cursor keeps its cell"
|
||||
);
|
||||
let (hidden, _) = shown(b"\x1b[8m\xe2\x94\x80");
|
||||
assert!(hidden.rows[0].alone.is_empty(), "hidden lines draw nothing");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
use std::ops::Range;
|
||||
|
||||
use alacritty_terminal::{grid::Scroll, term::TermMode};
|
||||
use gpui::{
|
||||
App, Bounds, Context, EntityInputHandler, KeyBinding, KeyDownEvent, KeyUpEvent, Keystroke,
|
||||
Pixels, Point, UTF16Selection, Window, actions,
|
||||
};
|
||||
|
||||
use super::{
|
||||
keys::{self, Action, Key, Kitty},
|
||||
view::TerminalView,
|
||||
};
|
||||
|
||||
actions!(terminal, [SendTab, SendBackTab]);
|
||||
|
||||
/// The key context a terminal's root sets.
|
||||
pub const CONTEXT: &str = "Terminal";
|
||||
|
||||
/// Tab and Shift-Tab reach the terminal before Ely moves focus with them.
|
||||
pub fn bind_keys(cx: &mut App) {
|
||||
cx.bind_keys([
|
||||
KeyBinding::new("tab", SendTab, Some(CONTEXT)),
|
||||
KeyBinding::new("shift-tab", SendBackTab, Some(CONTEXT)),
|
||||
]);
|
||||
}
|
||||
|
||||
impl TerminalView {
|
||||
/// Bytes from the keyboard: back to the newest line, then to the program.
|
||||
pub(super) fn input(&mut self, bytes: Vec<u8>, cx: &mut Context<Self>) {
|
||||
self.pty.term.lock().scroll_display(Scroll::Bottom);
|
||||
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;
|
||||
}
|
||||
let action = if event.is_held {
|
||||
Action::Repeat
|
||||
} else {
|
||||
Action::Press
|
||||
};
|
||||
if self.send(&event.keystroke, action, cx) {
|
||||
cx.stop_propagation();
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn key_up(&mut self, event: &KeyUpEvent, _: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.send(&event.keystroke, Action::Release, cx) {
|
||||
cx.stop_propagation();
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn tab(&mut self, shift: bool, cx: &mut Context<Self>) {
|
||||
let key = Key {
|
||||
name: "tab",
|
||||
text: if shift { "" } else { "\t" },
|
||||
shift,
|
||||
ctrl: false,
|
||||
alt: false,
|
||||
action: Action::Press,
|
||||
};
|
||||
self.encode(&key, cx);
|
||||
}
|
||||
|
||||
/// Sends what `keystroke` means to the program; false when it is text the input method types.
|
||||
fn send(&mut self, keystroke: &Keystroke, action: Action, cx: &mut Context<Self>) -> bool {
|
||||
let modifiers = keystroke.modifiers;
|
||||
if modifiers.platform {
|
||||
return false;
|
||||
}
|
||||
let alt = modifiers.alt && self.alt_is_meta(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,
|
||||
action,
|
||||
};
|
||||
self.encode(&key, cx)
|
||||
}
|
||||
|
||||
fn encode(&mut self, key: &Key, cx: &mut Context<Self>) -> bool {
|
||||
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
|
||||
}
|
||||
|
||||
/// Whether a held Alt is a modifier; on macOS only the Option keys the settings name.
|
||||
#[cfg(target_os = "macos")]
|
||||
fn alt_is_meta(&self, cx: &App) -> bool {
|
||||
use crate::settings::{OptionAsAlt, SettingsStore};
|
||||
let sides = || match super::mac::option_sides() {
|
||||
Ok(sides) => Some(sides),
|
||||
Err(error) => {
|
||||
log::error!("terminal {}: {error:#}; Option composes", self.pty.pid);
|
||||
None
|
||||
}
|
||||
};
|
||||
match SettingsStore::get(cx).option_as_alt {
|
||||
OptionAsAlt::Off => false,
|
||||
OptionAsAlt::Both => true,
|
||||
OptionAsAlt::Left => sides().is_some_and(|(left, _)| left),
|
||||
OptionAsAlt::Right => sides().is_some_and(|(_, right)| right),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn alt_is_meta(&self, _: &App) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
/// Whether a key went down, repeated, or came up.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Action {
|
||||
Press,
|
||||
Repeat,
|
||||
Release,
|
||||
}
|
||||
|
||||
/// A key as gpui names it, what it types, and the modifiers that count.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Key<'a> {
|
||||
/// gpui's name: `a`, `!`, `space`, `enter`, `f5`.
|
||||
pub name: &'a str,
|
||||
/// What the key types; empty when it types nothing.
|
||||
pub text: &'a str,
|
||||
pub shift: bool,
|
||||
pub ctrl: bool,
|
||||
/// Alt as a modifier, not as the key that composes characters.
|
||||
pub alt: bool,
|
||||
pub action: Action,
|
||||
}
|
||||
|
||||
/// The kitty keyboard flags a program set.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct Kitty {
|
||||
pub disambiguate: bool,
|
||||
pub events: bool,
|
||||
pub alternates: bool,
|
||||
pub all: bool,
|
||||
pub text: bool,
|
||||
}
|
||||
|
||||
/// The bytes a key sends; none when it sends nothing or its text is the input method's to type.
|
||||
pub fn encode(key: &Key, app_cursor: bool, kitty: Kitty) -> Option<Vec<u8>> {
|
||||
let on = kitty.disambiguate || kitty.events || kitty.alternates || kitty.all || kitty.text;
|
||||
if on {
|
||||
encode_kitty(key, kitty)
|
||||
} else {
|
||||
legacy(key, app_cursor)
|
||||
}
|
||||
}
|
||||
|
||||
/// xterm's modifier code: one plus shift 1, alt 2, ctrl 4.
|
||||
fn code(key: &Key) -> u32 {
|
||||
1 + u32::from(key.shift) + 2 * u32::from(key.alt) + 4 * u32::from(key.ctrl)
|
||||
}
|
||||
|
||||
/// The character a key name stands for, `space` included.
|
||||
fn printable(name: &str) -> Option<char> {
|
||||
let mut chars = name.chars();
|
||||
match (chars.next(), chars.next()) {
|
||||
(Some(ch), None) => Some(ch),
|
||||
_ => (name == "space").then_some(' '),
|
||||
}
|
||||
}
|
||||
|
||||
fn legacy(key: &Key, app_cursor: bool) -> Option<Vec<u8>> {
|
||||
if key.action == Action::Release {
|
||||
return None;
|
||||
}
|
||||
if let Some(sequence) = function(key, app_cursor) {
|
||||
return Some(sequence);
|
||||
}
|
||||
let ch = printable(key.name)?;
|
||||
if key.ctrl {
|
||||
if let Some(byte) = control(ch, key.shift) {
|
||||
return Some(if key.alt {
|
||||
vec![0x1B, byte]
|
||||
} else {
|
||||
vec![byte]
|
||||
});
|
||||
}
|
||||
return Some(format!("\x1b[{};{}u", ch as u32, code(key)).into_bytes());
|
||||
}
|
||||
if key.alt {
|
||||
let mut bytes = vec![0x1B];
|
||||
if key.text.is_empty() {
|
||||
let typed = if key.shift {
|
||||
ch.to_ascii_uppercase()
|
||||
} else {
|
||||
ch
|
||||
};
|
||||
bytes.extend(typed.to_string().as_bytes());
|
||||
} else {
|
||||
bytes.extend(key.text.as_bytes());
|
||||
}
|
||||
return Some(bytes);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// xterm's bytes for a function, cursor or editing key.
|
||||
fn function(key: &Key, app_cursor: bool) -> Option<Vec<u8>> {
|
||||
let mods = code(key);
|
||||
let modified = mods > 1;
|
||||
let cursor = |end: char| match (modified, app_cursor) {
|
||||
(true, _) => format!("\x1b[1;{mods}{end}"),
|
||||
(false, true) => format!("\x1bO{end}"),
|
||||
(false, false) => format!("\x1b[{end}"),
|
||||
};
|
||||
let tilde = |n: u32| {
|
||||
if modified {
|
||||
format!("\x1b[{n};{mods}~")
|
||||
} else {
|
||||
format!("\x1b[{n}~")
|
||||
}
|
||||
};
|
||||
let ss3 = |end: char| {
|
||||
if modified {
|
||||
format!("\x1b[1;{mods}{end}")
|
||||
} else {
|
||||
format!("\x1bO{end}")
|
||||
}
|
||||
};
|
||||
let only = (key.shift, key.alt, key.ctrl);
|
||||
let sequence = match key.name {
|
||||
"up" => cursor('A'),
|
||||
"down" => cursor('B'),
|
||||
"right" => cursor('C'),
|
||||
"left" => cursor('D'),
|
||||
"home" => cursor('H'),
|
||||
"end" => cursor('F'),
|
||||
"insert" => tilde(2),
|
||||
"delete" => tilde(3),
|
||||
"pageup" => tilde(5),
|
||||
"pagedown" => tilde(6),
|
||||
"f1" => ss3('P'),
|
||||
"f2" => ss3('Q'),
|
||||
"f3" if modified => tilde(13),
|
||||
"f3" => ss3('R'),
|
||||
"f4" => ss3('S'),
|
||||
"enter" => match only {
|
||||
(false, false, false) => "\r".into(),
|
||||
(false, true, false) => "\x1b\r".into(),
|
||||
_ => format!("\x1b[27;{mods};13~"),
|
||||
},
|
||||
"tab" => match only {
|
||||
(false, false, false) => "\t".into(),
|
||||
(true, false, false) => "\x1b[Z".into(),
|
||||
(false, true, false) => "\x1b\t".into(),
|
||||
_ => format!("\x1b[27;{mods};9~"),
|
||||
},
|
||||
"escape" => match only {
|
||||
(false, false, false) => "\x1b".into(),
|
||||
(false, true, false) => "\x1b\x1b".into(),
|
||||
_ => format!("\x1b[27;{mods};27~"),
|
||||
},
|
||||
"backspace" => {
|
||||
let byte = if key.ctrl { 0x08 } else { 0x7F };
|
||||
return Some(if key.alt {
|
||||
vec![0x1B, byte]
|
||||
} else {
|
||||
vec![byte]
|
||||
});
|
||||
}
|
||||
name => tilde(FUNCTION_TILDES.iter().find(|(f, _)| *f == name)?.1),
|
||||
};
|
||||
Some(sequence.into_bytes())
|
||||
}
|
||||
|
||||
/// F5 to F25 and the numbers xterm sends them with.
|
||||
const FUNCTION_TILDES: [(&str, u32); 21] = [
|
||||
("f5", 15),
|
||||
("f6", 17),
|
||||
("f7", 18),
|
||||
("f8", 19),
|
||||
("f9", 20),
|
||||
("f10", 21),
|
||||
("f11", 23),
|
||||
("f12", 24),
|
||||
("f13", 25),
|
||||
("f14", 26),
|
||||
("f15", 28),
|
||||
("f16", 29),
|
||||
("f17", 31),
|
||||
("f18", 32),
|
||||
("f19", 33),
|
||||
("f20", 34),
|
||||
("f21", 42),
|
||||
("f22", 43),
|
||||
("f23", 44),
|
||||
("f24", 45),
|
||||
("f25", 46),
|
||||
];
|
||||
|
||||
/// The C0 byte Ctrl turns a key into, as kitty and Ghostty do; `[`, `i` and `m` go as CSI u.
|
||||
fn control(ch: char, shift: bool) -> Option<u8> {
|
||||
if shift && ch != '@' {
|
||||
return None;
|
||||
}
|
||||
Some(match ch {
|
||||
' ' | '2' | '@' => 0,
|
||||
'a'..='h' | 'j' | 'k' | 'l' | 'n'..='z' => ch as u8 - b'a' + 1,
|
||||
'3' => 27,
|
||||
'4' | '\\' => 28,
|
||||
'5' | ']' => 29,
|
||||
'6' | '^' | '~' => 30,
|
||||
'7' | '/' | '_' => 31,
|
||||
'8' | '?' => 127,
|
||||
'0' | '1' | '9' => ch as u8,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// kitty's functional keys: the number and the final byte.
|
||||
fn functional(name: &str) -> Option<(u32, char)> {
|
||||
Some(match name {
|
||||
"escape" => (27, 'u'),
|
||||
"enter" => (13, 'u'),
|
||||
"tab" => (9, 'u'),
|
||||
"backspace" => (127, 'u'),
|
||||
"insert" => (2, '~'),
|
||||
"delete" => (3, '~'),
|
||||
"left" => (1, 'D'),
|
||||
"right" => (1, 'C'),
|
||||
"up" => (1, 'A'),
|
||||
"down" => (1, 'B'),
|
||||
"pageup" => (5, '~'),
|
||||
"pagedown" => (6, '~'),
|
||||
"home" => (1, 'H'),
|
||||
"end" => (1, 'F'),
|
||||
"f1" => (1, 'P'),
|
||||
"f2" => (1, 'Q'),
|
||||
"f3" => (13, '~'),
|
||||
"f4" => (1, 'S'),
|
||||
name => match FUNCTION_TILDES.iter().position(|(f, _)| *f == name)? {
|
||||
index @ 0..=7 => (FUNCTION_TILDES[index].1, '~'),
|
||||
index => (57376 + index as u32 - 8, 'u'),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn encode_kitty(key: &Key, kitty: Kitty) -> Option<Vec<u8>> {
|
||||
let control_text = key.text.chars().any(char::is_control);
|
||||
if key.action == Action::Release {
|
||||
let legacy = matches!(key.name, "enter" | "tab" | "backspace");
|
||||
if !kitty.events || (legacy && !kitty.all) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
let shift_typed = key.shift && !key.text.is_empty() && !control_text;
|
||||
let bound = key.ctrl || key.alt || (key.shift && !shift_typed);
|
||||
if !kitty.all && !bound && key.action != Action::Release {
|
||||
match key.name {
|
||||
"enter" => return Some(b"\r".to_vec()),
|
||||
"tab" => return Some(b"\t".to_vec()),
|
||||
"backspace" => return Some(vec![0x7F]),
|
||||
_ if !key.text.is_empty() && !control_text => return None,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let (number, end) = functional(key.name)
|
||||
.or_else(|| printable(key.name).map(|ch| (ch.to_ascii_lowercase() as u32, 'u')))?;
|
||||
let mods = code(key);
|
||||
let event = match key.action {
|
||||
Action::Press => 1,
|
||||
Action::Repeat => 2,
|
||||
Action::Release => 3,
|
||||
};
|
||||
let event = if kitty.events { event } else { 1 };
|
||||
let mut out = String::from("\x1b[");
|
||||
if end != 'u' && end != '~' {
|
||||
if event > 1 {
|
||||
out.push_str(&format!("1;{mods}:{event}"));
|
||||
} else if mods > 1 {
|
||||
out.push_str(&format!("1;{mods}"));
|
||||
}
|
||||
out.push(end);
|
||||
return Some(out.into_bytes());
|
||||
}
|
||||
out.push_str(&number.to_string());
|
||||
let shifted = key.text.chars().next().filter(|&typed| {
|
||||
kitty.alternates && key.shift && typed as u32 != number && !typed.is_control()
|
||||
});
|
||||
if let Some(shifted) = shifted {
|
||||
out.push_str(&format!(":{}", shifted as u32));
|
||||
}
|
||||
let wrote_mods = event > 1 || mods > 1;
|
||||
if event > 1 {
|
||||
out.push_str(&format!(";{mods}:{event}"));
|
||||
} else if mods > 1 {
|
||||
out.push_str(&format!(";{mods}"));
|
||||
}
|
||||
let text: Vec<String> = key
|
||||
.text
|
||||
.chars()
|
||||
.filter(|ch| !ch.is_control())
|
||||
.map(|ch| (ch as u32).to_string())
|
||||
.collect();
|
||||
if kitty.text && key.action != Action::Release && !key.ctrl && !key.alt && !text.is_empty() {
|
||||
out.push_str(if wrote_mods { ";" } else { ";;" });
|
||||
out.push_str(&text.join(":"));
|
||||
}
|
||||
out.push(end);
|
||||
Some(out.into_bytes())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,260 @@
|
||||
use super::*;
|
||||
|
||||
/// A key pressed with modifiers written as `ctrl+alt+shift+name`.
|
||||
fn key(spec: &'static str, text: &'static str) -> Key<'static> {
|
||||
let mut parts: Vec<&str> = spec.split('+').collect();
|
||||
let name = parts.pop().expect("a name");
|
||||
Key {
|
||||
name,
|
||||
text,
|
||||
shift: parts.contains(&"shift"),
|
||||
ctrl: parts.contains(&"ctrl"),
|
||||
alt: parts.contains(&"alt"),
|
||||
action: Action::Press,
|
||||
}
|
||||
}
|
||||
|
||||
fn legacy(key: Key, app_cursor: bool) -> Option<String> {
|
||||
encode(&key, app_cursor, Kitty::default()).map(|bytes| String::from_utf8(bytes).expect("utf-8"))
|
||||
}
|
||||
|
||||
fn kitty(key: Key, flags: Kitty) -> Option<String> {
|
||||
encode(&key, false, flags).map(|bytes| String::from_utf8(bytes).expect("utf-8"))
|
||||
}
|
||||
|
||||
const DISAMBIGUATE: Kitty = Kitty {
|
||||
disambiguate: true,
|
||||
events: false,
|
||||
alternates: false,
|
||||
all: false,
|
||||
text: false,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn plain_text_is_left_to_the_input_method() {
|
||||
assert_eq!(legacy(key("a", "a"), false), None);
|
||||
assert_eq!(legacy(key("shift+a", "A"), false), None);
|
||||
assert_eq!(legacy(key("space", " "), false), None);
|
||||
assert_eq!(legacy(key("ctrl+a", ""), false).as_deref(), Some("\x01"));
|
||||
let released = Key {
|
||||
action: Action::Release,
|
||||
..key("enter", "\n")
|
||||
};
|
||||
assert_eq!(legacy(released, false), None, "legacy sends no releases");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cursor_keys_follow_decckm_and_modifiers() {
|
||||
assert_eq!(legacy(key("up", ""), false).as_deref(), Some("\x1b[A"));
|
||||
assert_eq!(legacy(key("up", ""), true).as_deref(), Some("\x1bOA"));
|
||||
assert_eq!(
|
||||
legacy(key("ctrl+up", ""), true).as_deref(),
|
||||
Some("\x1b[1;5A")
|
||||
);
|
||||
assert_eq!(
|
||||
legacy(key("alt+shift+left", ""), false).as_deref(),
|
||||
Some("\x1b[1;4D")
|
||||
);
|
||||
assert_eq!(legacy(key("home", ""), true).as_deref(), Some("\x1bOH"));
|
||||
assert_eq!(legacy(key("end", ""), false).as_deref(), Some("\x1b[F"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn function_and_editing_keys_send_xterm_sequences() {
|
||||
let cases = [
|
||||
("f1", "\x1bOP"),
|
||||
("ctrl+f1", "\x1b[1;5P"),
|
||||
("f3", "\x1bOR"),
|
||||
("shift+f3", "\x1b[13;2~"),
|
||||
("f4", "\x1bOS"),
|
||||
("f5", "\x1b[15~"),
|
||||
("shift+f5", "\x1b[15;2~"),
|
||||
("f12", "\x1b[24~"),
|
||||
("f25", "\x1b[46~"),
|
||||
("insert", "\x1b[2~"),
|
||||
("delete", "\x1b[3~"),
|
||||
("ctrl+pageup", "\x1b[5;5~"),
|
||||
("pagedown", "\x1b[6~"),
|
||||
];
|
||||
for (spec, want) in cases {
|
||||
assert_eq!(
|
||||
legacy(key(spec, ""), false).as_deref(),
|
||||
Some(want),
|
||||
"{spec}"
|
||||
);
|
||||
}
|
||||
assert_eq!(legacy(key("f26", ""), false), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enter_tab_escape_and_backspace_carry_their_modifiers() {
|
||||
let cases: [(&str, &str, &[u8]); 15] = [
|
||||
("enter", "\n", b"\r"),
|
||||
("shift+enter", "\n", b"\x1b[27;2;13~"),
|
||||
("alt+enter", "\n", b"\x1b\r"),
|
||||
("ctrl+enter", "", b"\x1b[27;5;13~"),
|
||||
("tab", "\t", b"\t"),
|
||||
("shift+tab", "", b"\x1b[Z"),
|
||||
("alt+tab", "\t", b"\x1b\t"),
|
||||
("ctrl+tab", "", b"\x1b[27;5;9~"),
|
||||
("escape", "", b"\x1b"),
|
||||
("alt+escape", "", b"\x1b\x1b"),
|
||||
("shift+escape", "", b"\x1b[27;2;27~"),
|
||||
("backspace", "", b"\x7f"),
|
||||
("ctrl+backspace", "", b"\x08"),
|
||||
("alt+backspace", "", b"\x1b\x7f"),
|
||||
("ctrl+alt+backspace", "", b"\x1b\x08"),
|
||||
];
|
||||
for (spec, text, want) in cases {
|
||||
assert_eq!(
|
||||
encode(&key(spec, text), false, Kitty::default()).as_deref(),
|
||||
Some(want),
|
||||
"{spec}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctrl_makes_c0_bytes_or_fixterms_csi_u() {
|
||||
let bytes = |spec| encode(&key(spec, ""), false, Kitty::default());
|
||||
assert_eq!(bytes("ctrl+c"), Some(vec![0x03]));
|
||||
assert_eq!(bytes("ctrl+alt+c"), Some(vec![0x1B, 0x03]));
|
||||
assert_eq!(bytes("ctrl+space"), Some(vec![0x00]));
|
||||
assert_eq!(bytes("ctrl+_"), Some(vec![31]));
|
||||
assert_eq!(bytes("ctrl+?"), Some(vec![127]));
|
||||
assert_eq!(bytes("ctrl+2"), Some(vec![0x00]));
|
||||
assert_eq!(bytes("ctrl+["), Some(b"\x1b[91;5u".to_vec()));
|
||||
assert_eq!(bytes("ctrl+i"), Some(b"\x1b[105;5u".to_vec()));
|
||||
assert_eq!(bytes("ctrl+m"), Some(b"\x1b[109;5u".to_vec()));
|
||||
assert_eq!(bytes("ctrl+shift+a"), Some(b"\x1b[97;6u".to_vec()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alt_prefixes_escape_to_what_the_key_types() {
|
||||
assert_eq!(legacy(key("alt+a", "a"), false).as_deref(), Some("\x1ba"));
|
||||
assert_eq!(
|
||||
legacy(key("alt+shift+a", ""), false).as_deref(),
|
||||
Some("\x1bA")
|
||||
);
|
||||
assert_eq!(legacy(key("alt+e", "é"), false).as_deref(), Some("\x1bé"));
|
||||
assert_eq!(
|
||||
legacy(key("alt+space", " "), false).as_deref(),
|
||||
Some("\x1b ")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitty_disambiguate_keeps_text_and_the_legacy_three() {
|
||||
assert_eq!(kitty(key("a", "a"), DISAMBIGUATE), None);
|
||||
assert_eq!(kitty(key("shift+a", "A"), DISAMBIGUATE), None);
|
||||
assert_eq!(
|
||||
kitty(key("enter", "\n"), DISAMBIGUATE).as_deref(),
|
||||
Some("\r")
|
||||
);
|
||||
assert_eq!(kitty(key("tab", "\t"), DISAMBIGUATE).as_deref(), Some("\t"));
|
||||
assert_eq!(
|
||||
kitty(key("backspace", ""), DISAMBIGUATE).as_deref(),
|
||||
Some("\x7f")
|
||||
);
|
||||
assert_eq!(
|
||||
kitty(key("escape", ""), DISAMBIGUATE).as_deref(),
|
||||
Some("\x1b[27u")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitty_reports_modified_keys_as_csi_u() {
|
||||
let cases = [
|
||||
("ctrl+a", "", "\x1b[97;5u"),
|
||||
("alt+a", "a", "\x1b[97;3u"),
|
||||
("ctrl+shift+a", "", "\x1b[97;6u"),
|
||||
("shift+enter", "\n", "\x1b[13;2u"),
|
||||
("shift+tab", "", "\x1b[9;2u"),
|
||||
("up", "", "\x1b[A"),
|
||||
("ctrl+up", "", "\x1b[1;5A"),
|
||||
("f1", "", "\x1b[P"),
|
||||
("f5", "", "\x1b[15~"),
|
||||
("ctrl+f5", "", "\x1b[15;5~"),
|
||||
("f13", "", "\x1b[57376u"),
|
||||
("f25", "", "\x1b[57388u"),
|
||||
];
|
||||
for (spec, text, want) in cases {
|
||||
assert_eq!(
|
||||
kitty(key(spec, text), DISAMBIGUATE).as_deref(),
|
||||
Some(want),
|
||||
"{spec}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitty_events_report_repeats_and_releases() {
|
||||
let flags = Kitty {
|
||||
events: true,
|
||||
..DISAMBIGUATE
|
||||
};
|
||||
let with = |spec, text, action| Key {
|
||||
action,
|
||||
..key(spec, text)
|
||||
};
|
||||
assert_eq!(
|
||||
kitty(with("a", "a", Action::Repeat), flags),
|
||||
None,
|
||||
"a repeat types again"
|
||||
);
|
||||
assert_eq!(
|
||||
kitty(with("a", "a", Action::Release), flags).as_deref(),
|
||||
Some("\x1b[97;1:3u")
|
||||
);
|
||||
assert_eq!(
|
||||
kitty(with("ctrl+a", "", Action::Repeat), flags).as_deref(),
|
||||
Some("\x1b[97;5:2u")
|
||||
);
|
||||
assert_eq!(kitty(with("enter", "\n", Action::Release), flags), None);
|
||||
assert_eq!(
|
||||
kitty(with("up", "", Action::Release), flags).as_deref(),
|
||||
Some("\x1b[1;1:3A")
|
||||
);
|
||||
assert_eq!(kitty(with("a", "a", Action::Release), DISAMBIGUATE), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitty_report_all_alternates_and_text() {
|
||||
let all = Kitty {
|
||||
all: true,
|
||||
..DISAMBIGUATE
|
||||
};
|
||||
assert_eq!(kitty(key("a", "a"), all).as_deref(), Some("\x1b[97u"));
|
||||
assert_eq!(
|
||||
kitty(key("shift+a", "A"), all).as_deref(),
|
||||
Some("\x1b[97;2u")
|
||||
);
|
||||
assert_eq!(kitty(key("enter", "\n"), all).as_deref(), Some("\x1b[13u"));
|
||||
let alternates = Kitty {
|
||||
alternates: true,
|
||||
..all
|
||||
};
|
||||
assert_eq!(
|
||||
kitty(key("shift+a", "A"), alternates).as_deref(),
|
||||
Some("\x1b[97:65;2u")
|
||||
);
|
||||
let text = Kitty { text: true, ..all };
|
||||
assert_eq!(kitty(key("a", "a"), text).as_deref(), Some("\x1b[97;;97u"));
|
||||
assert_eq!(
|
||||
kitty(key("shift+a", "A"), text).as_deref(),
|
||||
Some("\x1b[97;2;65u")
|
||||
);
|
||||
assert_eq!(
|
||||
kitty(key("ctrl+a", ""), text).as_deref(),
|
||||
Some("\x1b[97;5u")
|
||||
);
|
||||
let released = Key {
|
||||
action: Action::Release,
|
||||
..key("enter", "\n")
|
||||
};
|
||||
let everything = Kitty {
|
||||
events: true,
|
||||
..all
|
||||
};
|
||||
assert_eq!(kitty(released, everything).as_deref(), Some("\x1b[13;1:3u"));
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
use objc::{
|
||||
class, msg_send,
|
||||
runtime::{NO, Object},
|
||||
sel, sel_impl,
|
||||
};
|
||||
|
||||
/// AppKit's event types for a key going down and coming up.
|
||||
const KEY_DOWN: u64 = 10;
|
||||
const KEY_UP: u64 = 11;
|
||||
|
||||
/// 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;
|
||||
|
||||
/// Which Option keys are down in the key event AppKit is handling: left, then right.
|
||||
pub fn option_sides() -> anyhow::Result<(bool, bool)> {
|
||||
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 Option from");
|
||||
let kind: u64 = msg_send![event, type];
|
||||
anyhow::ensure!(
|
||||
kind == KEY_DOWN || kind == KEY_UP,
|
||||
"macOS: the current event is type {kind}, not a key"
|
||||
);
|
||||
let flags: u64 = msg_send![event, modifierFlags];
|
||||
Ok((flags & LEFT_OPTION != 0, flags & RIGHT_OPTION != 0))
|
||||
}
|
||||
}
|
||||
|
||||
/// Turns off the accent menu a held key opens, so held keys repeat as in other terminals.
|
||||
pub fn repeat_held_keys() {
|
||||
unsafe {
|
||||
let key: *mut Object = msg_send![
|
||||
class!(NSString),
|
||||
stringWithUTF8String: c"ApplePressAndHoldEnabled".as_ptr()
|
||||
];
|
||||
let off: *mut Object = msg_send![class!(NSNumber), numberWithBool: NO];
|
||||
let defaults: *mut Object =
|
||||
msg_send![class!(NSDictionary), dictionaryWithObject: off forKey: key];
|
||||
let store: *mut Object = msg_send![class!(NSUserDefaults), standardUserDefaults];
|
||||
let _: () = msg_send![store, registerDefaults: defaults];
|
||||
}
|
||||
log::info!("macOS: held keys repeat");
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
mod boxes;
|
||||
pub mod frame;
|
||||
pub mod ink;
|
||||
pub mod input;
|
||||
pub mod keys;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod mac;
|
||||
mod paint;
|
||||
mod pty;
|
||||
pub mod shell;
|
||||
|
||||
+64
-11
@@ -2,9 +2,9 @@ use std::rc::Rc;
|
||||
|
||||
use alacritty_terminal::vte::ansi::CursorShape;
|
||||
use gpui::{
|
||||
Bounds, ContentMask, Entity, Font, FontId, FontStyle, FontWeight, Hsla, IntoElement,
|
||||
PathBuilder, Pixels, Point, Size, StrikethroughStyle, Styled, TextRun, UnderlineStyle, Window,
|
||||
canvas, fill, outline, point, px, size,
|
||||
Bounds, ContentMask, ElementInputHandler, Entity, Font, FontId, FontStyle, FontWeight, Hsla,
|
||||
IntoElement, PathBuilder, Pixels, Point, Size, StrikethroughStyle, Styled, TextRun,
|
||||
UnderlineStyle, Window, canvas, fill, outline, point, px, size,
|
||||
};
|
||||
|
||||
use super::{
|
||||
@@ -28,48 +28,101 @@ pub struct Look {
|
||||
pub rule: Pixels,
|
||||
pub cursor: Hsla,
|
||||
pub cursor_text: Hsla,
|
||||
/// The terminal's own text and ground, for text an input method composes.
|
||||
pub fg: Hsla,
|
||||
pub bg: Hsla,
|
||||
}
|
||||
|
||||
/// The grid fitted to its box: it sizes the pty, then paints each row and the cursor.
|
||||
/// The grid fitted to its box: it sizes the pty, then paints each row, the cursor and composed text.
|
||||
pub fn grid(
|
||||
view: Entity<TerminalView>,
|
||||
frame: Rc<Frame>,
|
||||
cursor: Option<(Cursor, CursorShape)>,
|
||||
preedit: Option<String>,
|
||||
look: Look,
|
||||
) -> impl IntoElement {
|
||||
let cell = look.cell;
|
||||
let (fit, input) = (view.clone(), view);
|
||||
let at = frame.caret;
|
||||
canvas(
|
||||
move |bounds, window, cx| {
|
||||
let columns = (bounds.size.width / cell.width).floor() as usize;
|
||||
let lines = (bounds.size.height / cell.height).floor() as usize;
|
||||
view.update(cx, |view, cx| {
|
||||
let origin = snapped(bounds.origin, window);
|
||||
fit.update(cx, |view, cx| {
|
||||
if let Some((row, column)) = at {
|
||||
let corner =
|
||||
origin + point(cell.width * column as f32, cell.height * row as f32);
|
||||
view.caret = Bounds::new(corner, cell);
|
||||
}
|
||||
if view.fit(columns, lines) {
|
||||
cx.notify();
|
||||
window.request_animation_frame();
|
||||
}
|
||||
});
|
||||
},
|
||||
move |bounds, _, window, _| {
|
||||
move |bounds, _, window, cx| {
|
||||
log::trace!(
|
||||
"terminal: paint {:?}, row 0 {:?}",
|
||||
bounds,
|
||||
frame.rows.first().map(|row| row.text.trim_end())
|
||||
);
|
||||
let scale = window.scale_factor();
|
||||
let snap = |length: Pixels| px((f32::from(length) * scale).round() / scale);
|
||||
let origin = point(snap(bounds.origin.x), snap(bounds.origin.y));
|
||||
let focus = input.read(cx).focus.clone();
|
||||
window.handle_input(&focus, ElementInputHandler::new(bounds, input.clone()), cx);
|
||||
let origin = snapped(bounds.origin, window);
|
||||
for (index, row) in frame.rows.iter().enumerate() {
|
||||
let top = origin + point(px(0.), cell.height * index as f32);
|
||||
paint_row(row, top, &look, window);
|
||||
}
|
||||
if let Some((cursor, shape)) = &cursor {
|
||||
paint_cursor(cursor, *shape, origin, &look, window);
|
||||
match (&preedit, at, &cursor) {
|
||||
(Some(text), Some(at), _) => paint_preedit(text, at, origin, &look, window),
|
||||
(None, _, Some((cursor, shape))) => {
|
||||
paint_cursor(cursor, *shape, origin, &look, window)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
},
|
||||
)
|
||||
.size_full()
|
||||
}
|
||||
|
||||
/// `origin` on a whole device pixel.
|
||||
fn snapped(origin: Point<Pixels>, window: &Window) -> Point<Pixels> {
|
||||
let scale = window.scale_factor();
|
||||
let snap = |length: Pixels| px((f32::from(length) * scale).round() / scale);
|
||||
point(snap(origin.x), snap(origin.y))
|
||||
}
|
||||
|
||||
/// Text an input method composes, over the cursor's cell on the terminal's ground, underlined.
|
||||
fn paint_preedit(
|
||||
text: &str,
|
||||
(row, column): (usize, usize),
|
||||
origin: Point<Pixels>,
|
||||
look: &Look,
|
||||
window: &mut Window,
|
||||
) {
|
||||
let cell = look.cell;
|
||||
let corner = origin + point(cell.width * column as f32, cell.height * row as f32);
|
||||
let style = Style {
|
||||
fg: look.fg,
|
||||
bg: None,
|
||||
bold: false,
|
||||
italic: false,
|
||||
underline: Underline::Single,
|
||||
underline_color: look.fg,
|
||||
strike: false,
|
||||
};
|
||||
let runs = runs(text, &style, look.fg, look, window);
|
||||
let width = window
|
||||
.text_system()
|
||||
.shape_line(text.to_string().into(), look.size, &runs, None)
|
||||
.width
|
||||
.max(cell.width);
|
||||
window.paint_quad(fill(Bounds::new(corner, size(width, cell.height)), look.bg));
|
||||
draw(text, &runs, corner, None, look, window);
|
||||
decorate(corner, width, &style, look, window);
|
||||
}
|
||||
|
||||
fn face(style: &Style, base: &Font) -> Font {
|
||||
Font {
|
||||
weight: if style.bold {
|
||||
|
||||
+27
-7
@@ -3,14 +3,15 @@ use std::{path::PathBuf, process::ExitStatus, rc::Rc, time::Duration};
|
||||
use alacritty_terminal::{event::Event, grid::Dimensions, vte::ansi::CursorShape};
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
ClipboardItem, Context, EventEmitter, FocusHandle, Focusable, Font, FontFeatures, FontStyle,
|
||||
FontWeight, IntoElement, ParentElement, Pixels, Render, Styled, Subscription, Task, Window,
|
||||
div, prelude::*, px, size,
|
||||
Bounds, ClipboardItem, Context, EventEmitter, FocusHandle, Focusable, Font, FontFeatures,
|
||||
FontStyle, FontWeight, IntoElement, ParentElement, Pixels, Render, Styled, Subscription, Task,
|
||||
Window, div, prelude::*, px, size,
|
||||
};
|
||||
|
||||
use super::{
|
||||
frame::frame,
|
||||
ink::Ink,
|
||||
input::{self, SendBackTab, SendTab},
|
||||
paint::{self, Look},
|
||||
pty::{Events, Pty, PtyEvent, config, fit},
|
||||
};
|
||||
@@ -39,8 +40,12 @@ pub enum TerminalEvent {
|
||||
|
||||
/// A program on a pseudo-terminal, drawn cell by cell in the theme and font the settings choose.
|
||||
pub struct TerminalView {
|
||||
pty: Pty,
|
||||
focus: FocusHandle,
|
||||
pub(super) pty: Pty,
|
||||
pub(super) focus: FocusHandle,
|
||||
/// Text an input method is composing, drawn at the cursor.
|
||||
pub(super) marked: Option<String>,
|
||||
/// The cursor's cell in the window, from the last layout.
|
||||
pub(super) caret: Bounds<Pixels>,
|
||||
size: (usize, usize),
|
||||
cell: (u16, u16),
|
||||
blinking: bool,
|
||||
@@ -69,6 +74,8 @@ impl TerminalView {
|
||||
Self {
|
||||
pty,
|
||||
focus: cx.focus_handle().tab_stop(true),
|
||||
marked: None,
|
||||
caret: Bounds::default(),
|
||||
size,
|
||||
cell: (8, 16),
|
||||
blinking: false,
|
||||
@@ -218,6 +225,8 @@ fn look(settings: &Settings, ink: &Ink, window: &Window) -> anyhow::Result<Look>
|
||||
font,
|
||||
cursor: ink.cursor,
|
||||
cursor_text: ink.cursor_text,
|
||||
fg: ink.fg,
|
||||
bg: ink.bg,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -248,8 +257,12 @@ impl Render for TerminalView {
|
||||
});
|
||||
let root = div()
|
||||
.id(("terminal", cx.entity_id()))
|
||||
.key_context("Terminal")
|
||||
.key_context(input::CONTEXT)
|
||||
.track_focus(&self.focus)
|
||||
.on_key_down(cx.listener(Self::key_down))
|
||||
.on_key_up(cx.listener(Self::key_up))
|
||||
.on_action(cx.listener(|view, _: &SendTab, _, cx| view.tab(false, cx)))
|
||||
.on_action(cx.listener(|view, _: &SendBackTab, _, cx| view.tab(true, cx)))
|
||||
.size_full()
|
||||
.px(px(settings.padding_x))
|
||||
.py(px(settings.padding_y))
|
||||
@@ -260,7 +273,14 @@ impl Render for TerminalView {
|
||||
f32::from(look.cell.width).round() as u16,
|
||||
f32::from(look.cell.height).round() as u16,
|
||||
);
|
||||
root.child(paint::grid(cx.entity(), Rc::new(shown), cursor, look))
|
||||
let preedit = self.marked.clone();
|
||||
root.child(paint::grid(
|
||||
cx.entity(),
|
||||
Rc::new(shown),
|
||||
cursor,
|
||||
preedit,
|
||||
look,
|
||||
))
|
||||
}
|
||||
Err(error) => {
|
||||
log::error!("terminal {}: {error:#}", self.pty.pid);
|
||||
|
||||
Reference in new issue
Block a user