logiguard fork: GPUI xdg-activation keyboard-focus serial fix
Some checks failed
Update All Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Triage Project Sync (#84) / Sync triage project (push) Has been cancelled
release_nightly / notify_on_failure (push) Has been cancelled
release_nightly / check_style (push) Has been cancelled
release_nightly / run_tests_windows (push) Has been cancelled
release_nightly / clippy_windows (push) Has been cancelled
release_nightly / bundle_linux_aarch64 (push) Has been cancelled
release_nightly / bundle_linux_x86_64 (push) Has been cancelled
release_nightly / bundle_mac_aarch64 (push) Has been cancelled
release_nightly / bundle_mac_x86_64 (push) Has been cancelled
release_nightly / bundle_windows_aarch64 (push) Has been cancelled
release_nightly / bundle_windows_x86_64 (push) Has been cancelled
release_nightly / build_nix_linux_x86_64 (push) Has been cancelled
release_nightly / build_nix_mac_aarch64 (push) Has been cancelled
release_nightly / update_nightly_tag (push) Has been cancelled
Hotfix Review Monitor / check-hotfix-reviews (push) Has been cancelled
Stale PR Review Reminder / check-stale-prs (push) Has been cancelled
Update Weekly Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Bump collab-staging Tag / update-collab-staging-tag (push) Has been cancelled
compliance_check / scheduled_compliance_check (push) Has been cancelled

Single-commit orphan branch: full zed-industries/zed @ 8c74db0 source tree
with a 3-file patch applied (no upstream history).

Patch (crates/gpui_linux/src/linux/wayland/):
  - serial.rs: add SerialKind::KeyboardEnter
  - client.rs: store wl_keyboard.enter serial; add latest_serial_of()
  - window.rs: activate() uses keyboard-enter serial (Mutter focus gate)

Mutter honors window activation only when the token carries the keyboard-
focus serial from wl_keyboard.enter; GPUI used a stale mouse-press serial.
See docs/tray-window-focus-wayland.md in logiguard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mohamad Khani
2026-07-14 02:22:17 +03:30
commit b72a46db68
3984 changed files with 1583326 additions and 0 deletions

View File

@@ -0,0 +1,56 @@
[package]
name = "terminal"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[features]
test-support = [
"collections/test-support",
"gpui/test-support",
"settings/test-support",
]
[lints]
workspace = true
[lib]
path = "src/terminal.rs"
doctest = false
[dependencies]
async-channel.workspace = true
alacritty_terminal.workspace = true
anyhow.workspace = true
collections.workspace = true
futures.workspace = true
futures-lite.workspace = true
gpui.workspace = true
itertools.workspace = true
libc.workspace = true
log.workspace = true
regex.workspace = true
release_channel.workspace = true
schemars.workspace = true
serde.workspace = true
settings.workspace = true
sysinfo.workspace = true
task.workspace = true
theme.workspace = true
theme_settings.workspace = true
thiserror.workspace = true
url.workspace = true
util.workspace = true
urlencoding.workspace = true
parking_lot.workspace = true
percent-encoding.workspace = true
[target.'cfg(windows)'.dependencies]
windows.workspace = true
[dev-dependencies]
gpui = { workspace = true, features = ["test-support"] }
rand.workspace = true
settings = { workspace = true, features = ["test-support"] }
util_macros.workspace = true

1
crates/terminal/LICENSE-GPL Symbolic link
View File

@@ -0,0 +1 @@
../../LICENSE-GPL

View File

@@ -0,0 +1,11 @@
use alacritty_terminal::vte::ansi::Rgb as AlacRgb;
use gpui::Rgba;
//Convenience method to convert from a GPUI color to an alacritty Rgb
pub fn to_alac_rgb(color: impl Into<Rgba>) -> AlacRgb {
let color = color.into();
let r = ((color.r * color.a) * 255.) as u8;
let g = ((color.g * color.a) * 255.) as u8;
let b = ((color.b * color.a) * 255.) as u8;
AlacRgb { r, g, b }
}

View File

@@ -0,0 +1,417 @@
use std::borrow::Cow;
/// The mappings defined in this file where created from reading the alacritty source
use alacritty_terminal::term::TermMode;
use gpui::Keystroke;
#[derive(Debug, PartialEq, Eq)]
enum AlacModifiers {
None,
Alt,
Ctrl,
Shift,
CtrlShift,
Other,
}
impl AlacModifiers {
fn new(ks: &Keystroke) -> Self {
match (
ks.modifiers.alt,
ks.modifiers.control,
ks.modifiers.shift,
ks.modifiers.platform,
) {
(false, false, false, false) => AlacModifiers::None,
(true, false, false, false) => AlacModifiers::Alt,
(false, true, false, false) => AlacModifiers::Ctrl,
(false, false, true, false) => AlacModifiers::Shift,
(false, true, true, false) => AlacModifiers::CtrlShift,
_ => AlacModifiers::Other,
}
}
fn any(&self) -> bool {
match &self {
AlacModifiers::None => false,
AlacModifiers::Alt => true,
AlacModifiers::Ctrl => true,
AlacModifiers::Shift => true,
AlacModifiers::CtrlShift => true,
AlacModifiers::Other => true,
}
}
}
pub fn to_esc_str(
keystroke: &Keystroke,
mode: &TermMode,
option_as_meta: bool,
) -> Option<Cow<'static, str>> {
let modifiers = AlacModifiers::new(keystroke);
// Manual Bindings including modifiers
let manual_esc_str: Option<&'static str> = match (keystroke.key.as_ref(), &modifiers) {
//Basic special keys
("tab", AlacModifiers::None) => Some("\x09"),
("escape", AlacModifiers::None) => Some("\x1b"),
("enter", AlacModifiers::None) => Some("\x0d"),
("enter", AlacModifiers::Shift) => Some("\x0a"),
("enter", AlacModifiers::Alt) => Some("\x1b\x0d"),
("backspace", AlacModifiers::None) => Some("\x7f"),
//Interesting escape codes
("tab", AlacModifiers::Shift) => Some("\x1b[Z"),
("backspace", AlacModifiers::Ctrl) => Some("\x08"),
("backspace", AlacModifiers::Alt) => Some("\x1b\x7f"),
("backspace", AlacModifiers::Shift) => Some("\x7f"),
("space", AlacModifiers::Ctrl) => Some("\x00"),
("home", AlacModifiers::None) if mode.contains(TermMode::APP_CURSOR) => Some("\x1bOH"),
("home", AlacModifiers::None) if !mode.contains(TermMode::APP_CURSOR) => Some("\x1b[H"),
("end", AlacModifiers::None) if mode.contains(TermMode::APP_CURSOR) => Some("\x1bOF"),
("end", AlacModifiers::None) if !mode.contains(TermMode::APP_CURSOR) => Some("\x1b[F"),
("up", AlacModifiers::None) if mode.contains(TermMode::APP_CURSOR) => Some("\x1bOA"),
("up", AlacModifiers::None) if !mode.contains(TermMode::APP_CURSOR) => Some("\x1b[A"),
("down", AlacModifiers::None) if mode.contains(TermMode::APP_CURSOR) => Some("\x1bOB"),
("down", AlacModifiers::None) if !mode.contains(TermMode::APP_CURSOR) => Some("\x1b[B"),
("right", AlacModifiers::None) if mode.contains(TermMode::APP_CURSOR) => Some("\x1bOC"),
("right", AlacModifiers::None) if !mode.contains(TermMode::APP_CURSOR) => Some("\x1b[C"),
("left", AlacModifiers::None) if mode.contains(TermMode::APP_CURSOR) => Some("\x1bOD"),
("left", AlacModifiers::None) if !mode.contains(TermMode::APP_CURSOR) => Some("\x1b[D"),
("back", AlacModifiers::None) => Some("\x7f"),
("insert", AlacModifiers::None) => Some("\x1b[2~"),
("delete", AlacModifiers::None) => Some("\x1b[3~"),
("pageup", AlacModifiers::None) => Some("\x1b[5~"),
("pagedown", AlacModifiers::None) => Some("\x1b[6~"),
("f1", AlacModifiers::None) => Some("\x1bOP"),
("f2", AlacModifiers::None) => Some("\x1bOQ"),
("f3", AlacModifiers::None) => Some("\x1bOR"),
("f4", AlacModifiers::None) => Some("\x1bOS"),
("f5", AlacModifiers::None) => Some("\x1b[15~"),
("f6", AlacModifiers::None) => Some("\x1b[17~"),
("f7", AlacModifiers::None) => Some("\x1b[18~"),
("f8", AlacModifiers::None) => Some("\x1b[19~"),
("f9", AlacModifiers::None) => Some("\x1b[20~"),
("f10", AlacModifiers::None) => Some("\x1b[21~"),
("f11", AlacModifiers::None) => Some("\x1b[23~"),
("f12", AlacModifiers::None) => Some("\x1b[24~"),
("f13", AlacModifiers::None) => Some("\x1b[25~"),
("f14", AlacModifiers::None) => Some("\x1b[26~"),
("f15", AlacModifiers::None) => Some("\x1b[28~"),
("f16", AlacModifiers::None) => Some("\x1b[29~"),
("f17", AlacModifiers::None) => Some("\x1b[31~"),
("f18", AlacModifiers::None) => Some("\x1b[32~"),
("f19", AlacModifiers::None) => Some("\x1b[33~"),
("f20", AlacModifiers::None) => Some("\x1b[34~"),
// NumpadEnter, Action::Esc("\n".into());
//Mappings for caret notation keys
("a", AlacModifiers::Ctrl) => Some("\x01"), //1
("A", AlacModifiers::CtrlShift) => Some("\x01"), //1
("b", AlacModifiers::Ctrl) => Some("\x02"), //2
("B", AlacModifiers::CtrlShift) => Some("\x02"), //2
("c", AlacModifiers::Ctrl) => Some("\x03"), //3
("C", AlacModifiers::CtrlShift) => Some("\x03"), //3
("d", AlacModifiers::Ctrl) => Some("\x04"), //4
("D", AlacModifiers::CtrlShift) => Some("\x04"), //4
("e", AlacModifiers::Ctrl) => Some("\x05"), //5
("E", AlacModifiers::CtrlShift) => Some("\x05"), //5
("f", AlacModifiers::Ctrl) => Some("\x06"), //6
("F", AlacModifiers::CtrlShift) => Some("\x06"), //6
("g", AlacModifiers::Ctrl) => Some("\x07"), //7
("G", AlacModifiers::CtrlShift) => Some("\x07"), //7
("h", AlacModifiers::Ctrl) => Some("\x08"), //8
("H", AlacModifiers::CtrlShift) => Some("\x08"), //8
("i", AlacModifiers::Ctrl) => Some("\x09"), //9
("I", AlacModifiers::CtrlShift) => Some("\x09"), //9
("j", AlacModifiers::Ctrl) => Some("\x0a"), //10
("J", AlacModifiers::CtrlShift) => Some("\x0a"), //10
("k", AlacModifiers::Ctrl) => Some("\x0b"), //11
("K", AlacModifiers::CtrlShift) => Some("\x0b"), //11
("l", AlacModifiers::Ctrl) => Some("\x0c"), //12
("L", AlacModifiers::CtrlShift) => Some("\x0c"), //12
("m", AlacModifiers::Ctrl) => Some("\x0d"), //13
("M", AlacModifiers::CtrlShift) => Some("\x0d"), //13
("n", AlacModifiers::Ctrl) => Some("\x0e"), //14
("N", AlacModifiers::CtrlShift) => Some("\x0e"), //14
("o", AlacModifiers::Ctrl) => Some("\x0f"), //15
("O", AlacModifiers::CtrlShift) => Some("\x0f"), //15
("p", AlacModifiers::Ctrl) => Some("\x10"), //16
("P", AlacModifiers::CtrlShift) => Some("\x10"), //16
("q", AlacModifiers::Ctrl) => Some("\x11"), //17
("Q", AlacModifiers::CtrlShift) => Some("\x11"), //17
("r", AlacModifiers::Ctrl) => Some("\x12"), //18
("R", AlacModifiers::CtrlShift) => Some("\x12"), //18
("s", AlacModifiers::Ctrl) => Some("\x13"), //19
("S", AlacModifiers::CtrlShift) => Some("\x13"), //19
("t", AlacModifiers::Ctrl) => Some("\x14"), //20
("T", AlacModifiers::CtrlShift) => Some("\x14"), //20
("u", AlacModifiers::Ctrl) => Some("\x15"), //21
("U", AlacModifiers::CtrlShift) => Some("\x15"), //21
("v", AlacModifiers::Ctrl) => Some("\x16"), //22
("V", AlacModifiers::CtrlShift) => Some("\x16"), //22
("w", AlacModifiers::Ctrl) => Some("\x17"), //23
("W", AlacModifiers::CtrlShift) => Some("\x17"), //23
("x", AlacModifiers::Ctrl) => Some("\x18"), //24
("X", AlacModifiers::CtrlShift) => Some("\x18"), //24
("y", AlacModifiers::Ctrl) => Some("\x19"), //25
("Y", AlacModifiers::CtrlShift) => Some("\x19"), //25
("z", AlacModifiers::Ctrl) => Some("\x1a"), //26
("Z", AlacModifiers::CtrlShift) => Some("\x1a"), //26
("@", AlacModifiers::Ctrl) => Some("\x00"), //0
("[", AlacModifiers::Ctrl) => Some("\x1b"), //27
("\\", AlacModifiers::Ctrl) => Some("\x1c"), //28
("]", AlacModifiers::Ctrl) => Some("\x1d"), //29
("^", AlacModifiers::Ctrl) => Some("\x1e"), //30
("_", AlacModifiers::Ctrl) => Some("\x1f"), //31
("?", AlacModifiers::Ctrl) => Some("\x7f"), //127
_ => None,
};
if let Some(esc_str) = manual_esc_str {
return Some(Cow::Borrowed(esc_str));
}
// Automated bindings applying modifiers
if modifiers.any() {
let modifier_code = modifier_code(keystroke);
let modified_esc_str = match keystroke.key.as_ref() {
"up" => Some(format!("\x1b[1;{}A", modifier_code)),
"down" => Some(format!("\x1b[1;{}B", modifier_code)),
"right" => Some(format!("\x1b[1;{}C", modifier_code)),
"left" => Some(format!("\x1b[1;{}D", modifier_code)),
"f1" => Some(format!("\x1b[1;{}P", modifier_code)),
"f2" => Some(format!("\x1b[1;{}Q", modifier_code)),
"f3" => Some(format!("\x1b[1;{}R", modifier_code)),
"f4" => Some(format!("\x1b[1;{}S", modifier_code)),
"F5" => Some(format!("\x1b[15;{}~", modifier_code)),
"f6" => Some(format!("\x1b[17;{}~", modifier_code)),
"f7" => Some(format!("\x1b[18;{}~", modifier_code)),
"f8" => Some(format!("\x1b[19;{}~", modifier_code)),
"f9" => Some(format!("\x1b[20;{}~", modifier_code)),
"f10" => Some(format!("\x1b[21;{}~", modifier_code)),
"f11" => Some(format!("\x1b[23;{}~", modifier_code)),
"f12" => Some(format!("\x1b[24;{}~", modifier_code)),
"f13" => Some(format!("\x1b[25;{}~", modifier_code)),
"f14" => Some(format!("\x1b[26;{}~", modifier_code)),
"f15" => Some(format!("\x1b[28;{}~", modifier_code)),
"f16" => Some(format!("\x1b[29;{}~", modifier_code)),
"f17" => Some(format!("\x1b[31;{}~", modifier_code)),
"f18" => Some(format!("\x1b[32;{}~", modifier_code)),
"f19" => Some(format!("\x1b[33;{}~", modifier_code)),
"f20" => Some(format!("\x1b[34;{}~", modifier_code)),
"insert" => Some(format!("\x1b[2;{}~", modifier_code)),
"pageup" => Some(format!("\x1b[5;{}~", modifier_code)),
"pagedown" => Some(format!("\x1b[6;{}~", modifier_code)),
"end" => Some(format!("\x1b[1;{}F", modifier_code)),
"home" => Some(format!("\x1b[1;{}H", modifier_code)),
_ => None,
};
if let Some(esc_str) = modified_esc_str {
return Some(Cow::Owned(esc_str));
}
}
if !cfg!(target_os = "macos") || option_as_meta {
let is_alt_lowercase_ascii = modifiers == AlacModifiers::Alt && keystroke.key.is_ascii();
let is_alt_uppercase_ascii =
keystroke.modifiers.alt && keystroke.modifiers.shift && keystroke.key.is_ascii();
if is_alt_lowercase_ascii || is_alt_uppercase_ascii {
let key = if is_alt_uppercase_ascii {
&keystroke.key.to_ascii_uppercase()
} else {
&keystroke.key
};
return Some(Cow::Owned(format!("\x1b{}", key)));
}
}
None
}
/// Code Modifiers
/// ---------+---------------------------
/// 2 | Shift
/// 3 | Alt
/// 4 | Shift + Alt
/// 5 | Control
/// 6 | Shift + Control
/// 7 | Alt + Control
/// 8 | Shift + Alt + Control
/// ---------+---------------------------
/// from: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-PC-Style-Function-Keys
fn modifier_code(keystroke: &Keystroke) -> u32 {
let mut modifier_code = 0;
if keystroke.modifiers.shift {
modifier_code |= 1;
}
if keystroke.modifiers.alt {
modifier_code |= 1 << 1;
}
if keystroke.modifiers.control {
modifier_code |= 1 << 2;
}
modifier_code + 1
}
#[cfg(test)]
mod test {
use gpui::Modifiers;
use super::*;
#[test]
fn test_plain_inputs() {
let ks = Keystroke {
modifiers: Modifiers {
control: false,
alt: false,
shift: false,
platform: false,
function: false,
},
key: "🖖🏻".to_string(), //2 char string
key_char: None,
};
assert_eq!(to_esc_str(&ks, &TermMode::NONE, false), None);
}
#[test]
fn test_application_mode() {
let app_cursor = TermMode::APP_CURSOR;
let none = TermMode::NONE;
let up = Keystroke::parse("up").unwrap();
let down = Keystroke::parse("down").unwrap();
let left = Keystroke::parse("left").unwrap();
let right = Keystroke::parse("right").unwrap();
assert_eq!(to_esc_str(&up, &none, false), Some("\x1b[A".into()));
assert_eq!(to_esc_str(&down, &none, false), Some("\x1b[B".into()));
assert_eq!(to_esc_str(&right, &none, false), Some("\x1b[C".into()));
assert_eq!(to_esc_str(&left, &none, false), Some("\x1b[D".into()));
assert_eq!(to_esc_str(&up, &app_cursor, false), Some("\x1bOA".into()));
assert_eq!(to_esc_str(&down, &app_cursor, false), Some("\x1bOB".into()));
assert_eq!(
to_esc_str(&right, &app_cursor, false),
Some("\x1bOC".into())
);
assert_eq!(to_esc_str(&left, &app_cursor, false), Some("\x1bOD".into()));
let home = Keystroke::parse("home").unwrap();
let end = Keystroke::parse("end").unwrap();
assert_eq!(to_esc_str(&home, &none, false), Some("\x1b[H".into()));
assert_eq!(to_esc_str(&end, &none, false), Some("\x1b[F".into()));
assert_eq!(to_esc_str(&home, &app_cursor, false), Some("\x1bOH".into()));
assert_eq!(to_esc_str(&end, &app_cursor, false), Some("\x1bOF".into()));
let shift_home = Keystroke::parse("shift-home").unwrap();
let shift_end = Keystroke::parse("shift-end").unwrap();
assert_eq!(
to_esc_str(&shift_home, &none, false),
Some("\x1b[1;2H".into())
);
assert_eq!(
to_esc_str(&shift_end, &none, false),
Some("\x1b[1;2F".into())
);
}
#[test]
fn test_ctrl_codes() {
let letters_lower = 'a'..='z';
let letters_upper = 'A'..='Z';
let mode = TermMode::ANY;
for (lower, upper) in letters_lower.zip(letters_upper) {
assert_eq!(
to_esc_str(
&Keystroke::parse(&format!("ctrl-shift-{}", lower)).unwrap(),
&mode,
false
),
to_esc_str(
&Keystroke::parse(&format!("ctrl-{}", upper)).unwrap(),
&mode,
false
),
"On letter: {}/{}",
lower,
upper
)
}
}
#[test]
fn alt_is_meta() {
let ascii_printable = ' '..='~';
for character in ascii_printable {
assert_eq!(
to_esc_str(
&Keystroke::parse(&format!("alt-{}", character)).unwrap(),
&TermMode::NONE,
true
)
.unwrap(),
format!("\x1b{}", character)
);
}
let gpui_keys = [
"up", "down", "right", "left", "f1", "f2", "f3", "f4", "F5", "f6", "f7", "f8", "f9",
"f10", "f11", "f12", "f13", "f14", "f15", "f16", "f17", "f18", "f19", "f20", "insert",
"pageup", "pagedown", "end", "home",
];
for key in gpui_keys {
assert_ne!(
to_esc_str(
&Keystroke::parse(&format!("alt-{}", key)).unwrap(),
&TermMode::NONE,
true
)
.unwrap(),
format!("\x1b{}", key)
);
}
}
#[test]
fn test_shift_enter_newline() {
let shift_enter = Keystroke::parse("shift-enter").unwrap();
let regular_enter = Keystroke::parse("enter").unwrap();
let mode = TermMode::NONE;
// Shift-enter should send line feed (newline)
assert_eq!(to_esc_str(&shift_enter, &mode, false), Some("\x0a".into()));
// Regular enter should still send carriage return
assert_eq!(
to_esc_str(&regular_enter, &mode, false),
Some("\x0d".into())
);
}
#[test]
fn test_modifier_code_calc() {
// Code Modifiers
// ---------+---------------------------
// 2 | Shift
// 3 | Alt
// 4 | Shift + Alt
// 5 | Control
// 6 | Shift + Control
// 7 | Alt + Control
// 8 | Shift + Alt + Control
// ---------+---------------------------
// from: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-PC-Style-Function-Keys
assert_eq!(2, modifier_code(&Keystroke::parse("shift-a").unwrap()));
assert_eq!(3, modifier_code(&Keystroke::parse("alt-a").unwrap()));
assert_eq!(4, modifier_code(&Keystroke::parse("shift-alt-a").unwrap()));
assert_eq!(5, modifier_code(&Keystroke::parse("ctrl-a").unwrap()));
assert_eq!(6, modifier_code(&Keystroke::parse("shift-ctrl-a").unwrap()));
assert_eq!(7, modifier_code(&Keystroke::parse("alt-ctrl-a").unwrap()));
assert_eq!(
8,
modifier_code(&Keystroke::parse("shift-ctrl-alt-a").unwrap())
);
}
}

View File

@@ -0,0 +1,3 @@
pub mod colors;
pub mod keys;
pub mod mouse;

View File

@@ -0,0 +1,319 @@
use std::cmp::{self, min};
use std::iter::repeat;
use alacritty_terminal::grid::Dimensions;
/// Most of the code, and specifically the constants, in this are copied from Alacritty,
/// with modifications for our circumstances
use alacritty_terminal::index::{Column as GridCol, Line as GridLine, Point as AlacPoint, Side};
use alacritty_terminal::term::TermMode;
use gpui::{Modifiers, MouseButton, Pixels, Point, ScrollWheelEvent, px};
use crate::TerminalBounds;
enum MouseFormat {
Sgr,
Normal(bool),
}
impl MouseFormat {
fn from_mode(mode: TermMode) -> Self {
if mode.contains(TermMode::SGR_MOUSE) {
MouseFormat::Sgr
} else if mode.contains(TermMode::UTF8_MOUSE) {
MouseFormat::Normal(true)
} else {
MouseFormat::Normal(false)
}
}
}
#[derive(Debug)]
enum AlacMouseButton {
LeftButton = 0,
MiddleButton = 1,
RightButton = 2,
LeftMove = 32,
MiddleMove = 33,
RightMove = 34,
NoneMove = 35,
ScrollUp = 64,
ScrollDown = 65,
Other = 99,
}
impl AlacMouseButton {
fn from_move_button(e: Option<MouseButton>) -> Self {
match e {
Some(gpui::MouseButton::Left) => AlacMouseButton::LeftMove,
Some(gpui::MouseButton::Middle) => AlacMouseButton::MiddleMove,
Some(gpui::MouseButton::Right) => AlacMouseButton::RightMove,
Some(gpui::MouseButton::Navigate(_)) => AlacMouseButton::Other,
None => AlacMouseButton::NoneMove,
}
}
fn from_button(e: MouseButton) -> Self {
match e {
gpui::MouseButton::Left => AlacMouseButton::LeftButton,
gpui::MouseButton::Right => AlacMouseButton::MiddleButton,
gpui::MouseButton::Middle => AlacMouseButton::RightButton,
gpui::MouseButton::Navigate(_) => AlacMouseButton::Other,
}
}
fn from_scroll(e: &ScrollWheelEvent) -> Self {
let is_positive = match e.delta {
gpui::ScrollDelta::Pixels(pixels) => pixels.y > px(0.),
gpui::ScrollDelta::Lines(lines) => lines.y > 0.,
};
if is_positive {
AlacMouseButton::ScrollUp
} else {
AlacMouseButton::ScrollDown
}
}
fn is_other(&self) -> bool {
matches!(self, AlacMouseButton::Other)
}
}
pub fn scroll_report(
point: AlacPoint,
scroll_lines: i32,
e: &ScrollWheelEvent,
mode: TermMode,
) -> Option<impl Iterator<Item = Vec<u8>>> {
if mode.intersects(TermMode::MOUSE_MODE) {
mouse_report(
point,
AlacMouseButton::from_scroll(e),
true,
e.modifiers,
MouseFormat::from_mode(mode),
)
.map(|report| repeat(report).take(scroll_lines.unsigned_abs() as usize))
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use gpui::{ScrollDelta, TouchPhase, point};
#[test]
fn scroll_report_repeats_for_negative_scroll_lines() {
let grid_point = AlacPoint::new(GridLine(0), GridCol(0));
let scroll_event = ScrollWheelEvent {
delta: ScrollDelta::Lines(point(0., -1.)),
touch_phase: TouchPhase::Moved,
..Default::default()
};
let mode = TermMode::MOUSE_MODE;
let reports: Vec<Vec<u8>> = scroll_report(grid_point, -3, &scroll_event, mode)
.expect("mouse mode should produce a scroll report")
.collect();
assert_eq!(reports.len(), 3);
}
#[test]
fn scroll_report_repeats_for_positive_scroll_lines() {
let grid_point = AlacPoint::new(GridLine(0), GridCol(0));
let scroll_event = ScrollWheelEvent {
delta: ScrollDelta::Lines(point(0., 1.)),
touch_phase: TouchPhase::Moved,
..Default::default()
};
let mode = TermMode::MOUSE_MODE;
let reports: Vec<Vec<u8>> = scroll_report(grid_point, 3, &scroll_event, mode)
.expect("mouse mode should produce a scroll report")
.collect();
assert_eq!(reports.len(), 3);
}
}
pub fn alt_scroll(scroll_lines: i32) -> Vec<u8> {
let cmd = if scroll_lines > 0 { b'A' } else { b'B' };
let mut content = Vec::with_capacity(scroll_lines.unsigned_abs() as usize * 3);
for _ in 0..scroll_lines.abs() {
content.push(0x1b);
content.push(b'O');
content.push(cmd);
}
content
}
pub fn mouse_button_report(
point: AlacPoint,
button: gpui::MouseButton,
modifiers: Modifiers,
pressed: bool,
mode: TermMode,
) -> Option<Vec<u8>> {
let button = AlacMouseButton::from_button(button);
if !button.is_other() && mode.intersects(TermMode::MOUSE_MODE) {
mouse_report(
point,
button,
pressed,
modifiers,
MouseFormat::from_mode(mode),
)
} else {
None
}
}
pub fn mouse_moved_report(
point: AlacPoint,
button: Option<MouseButton>,
modifiers: Modifiers,
mode: TermMode,
) -> Option<Vec<u8>> {
let button = AlacMouseButton::from_move_button(button);
if !button.is_other() && mode.intersects(TermMode::MOUSE_MOTION | TermMode::MOUSE_DRAG) {
//Only drags are reported in drag mode, so block NoneMove.
if mode.contains(TermMode::MOUSE_DRAG) && matches!(button, AlacMouseButton::NoneMove) {
None
} else {
mouse_report(point, button, true, modifiers, MouseFormat::from_mode(mode))
}
} else {
None
}
}
pub fn grid_point(
pos: Point<Pixels>,
cur_size: TerminalBounds,
display_offset: usize,
) -> AlacPoint {
grid_point_and_side(pos, cur_size, display_offset).0
}
pub fn grid_point_and_side(
pos: Point<Pixels>,
cur_size: TerminalBounds,
display_offset: usize,
) -> (AlacPoint, Side) {
let mut col = GridCol((pos.x / cur_size.cell_width) as usize);
let cell_x = cmp::max(px(0.), pos.x) % cur_size.cell_width;
let half_cell_width = cur_size.cell_width / 2.0;
let mut side = if cell_x > half_cell_width {
Side::Right
} else {
Side::Left
};
if col > cur_size.last_column() {
col = cur_size.last_column();
side = Side::Right;
}
let col = min(col, cur_size.last_column());
let mut line = (pos.y / cur_size.line_height) as i32;
if line > cur_size.bottommost_line() {
line = cur_size.bottommost_line().0;
side = Side::Right;
} else if line < 0 {
side = Side::Left;
}
(
AlacPoint::new(GridLine(line - display_offset as i32), col),
side,
)
}
///Generate the bytes to send to the terminal, from the cell location, a mouse event, and the terminal mode
fn mouse_report(
point: AlacPoint,
button: AlacMouseButton,
pressed: bool,
modifiers: Modifiers,
format: MouseFormat,
) -> Option<Vec<u8>> {
if point.line < 0 {
return None;
}
let mut mods = 0;
if modifiers.shift {
mods += 4;
}
if modifiers.alt {
mods += 8;
}
if modifiers.control {
mods += 16;
}
match format {
MouseFormat::Sgr => {
Some(sgr_mouse_report(point, button as u8 + mods, pressed).into_bytes())
}
MouseFormat::Normal(utf8) => {
if pressed {
normal_mouse_report(point, button as u8 + mods, utf8)
} else {
normal_mouse_report(point, 3 + mods, utf8)
}
}
}
}
fn normal_mouse_report(point: AlacPoint, button: u8, utf8: bool) -> Option<Vec<u8>> {
let AlacPoint { line, column } = point;
let max_point = if utf8 { 2015 } else { 223 };
if line >= max_point || column >= max_point {
return None;
}
let mut msg = vec![b'\x1b', b'[', b'M', 32 + button];
let mouse_pos_encode = |pos: usize| -> Vec<u8> {
let pos = 32 + 1 + pos;
let first = 0xC0 + pos / 64;
let second = 0x80 + (pos & 63);
vec![first as u8, second as u8]
};
if utf8 && column >= 95 {
msg.append(&mut mouse_pos_encode(column.0));
} else {
msg.push(32 + 1 + column.0 as u8);
}
if utf8 && line >= 95 {
msg.append(&mut mouse_pos_encode(line.0 as usize));
} else {
msg.push(32 + 1 + line.0 as u8);
}
Some(msg)
}
fn sgr_mouse_report(point: AlacPoint, button: u8, pressed: bool) -> String {
let c = if pressed { 'M' } else { 'm' };
let msg = format!(
"\x1b[<{};{};{}{}",
button,
point.column + 1,
point.line + 1,
c
);
msg
}

View File

@@ -0,0 +1,220 @@
use alacritty_terminal::tty::Pty;
use gpui::{Context, Task};
use parking_lot::{MappedRwLockReadGuard, Mutex, RwLock, RwLockReadGuard};
#[cfg(target_os = "windows")]
use std::num::NonZeroU32;
#[cfg(unix)]
use std::os::fd::AsRawFd;
use std::{path::PathBuf, sync::Arc};
#[cfg(target_os = "windows")]
use windows::Win32::{Foundation::HANDLE, System::Threading::GetProcessId};
use sysinfo::{Pid, Process, ProcessRefreshKind, RefreshKind, System, UpdateKind};
use crate::{Event, Terminal};
#[derive(Clone, Copy)]
pub struct ProcessIdGetter {
handle: i32,
fallback_pid: u32,
}
impl ProcessIdGetter {
pub fn fallback_pid(&self) -> Pid {
Pid::from_u32(self.fallback_pid)
}
}
#[cfg(unix)]
impl ProcessIdGetter {
fn new(pty: &Pty) -> ProcessIdGetter {
ProcessIdGetter {
handle: pty.file().as_raw_fd(),
fallback_pid: pty.child().id(),
}
}
fn pid(&self) -> Option<Pid> {
// Negative pid means error.
// Zero pid means no foreground process group is set on the PTY yet.
// Avoid killing the current process by returning a zero pid.
let pid = unsafe { libc::tcgetpgrp(self.handle) };
if pid > 0 {
return Some(Pid::from_u32(pid as u32));
}
if self.fallback_pid > 0 {
return Some(Pid::from_u32(self.fallback_pid));
}
None
}
}
#[cfg(windows)]
impl ProcessIdGetter {
fn new(pty: &Pty) -> ProcessIdGetter {
let child = pty.child_watcher();
let handle = child.raw_handle();
let fallback_pid = child.pid().unwrap_or_else(|| unsafe {
NonZeroU32::new_unchecked(GetProcessId(HANDLE(handle as _)))
});
ProcessIdGetter {
handle: handle as i32,
fallback_pid: u32::from(fallback_pid),
}
}
fn pid(&self) -> Option<Pid> {
let pid = unsafe { GetProcessId(HANDLE(self.handle as _)) };
// the GetProcessId may fail and returns zero, which will lead to a stack overflow issue
if pid == 0 {
// in the builder process, there is a small chance, almost negligible,
// that this value could be zero, which means child_watcher returns None,
// GetProcessId returns 0.
if self.fallback_pid == 0 {
return None;
}
return Some(Pid::from_u32(self.fallback_pid));
}
Some(Pid::from_u32(pid))
}
}
#[derive(Clone, Debug)]
pub struct ProcessInfo {
pub name: String,
pub cwd: PathBuf,
pub argv: Vec<String>,
}
/// Fetches Zed-relevant Pseudo-Terminal (PTY) process information
pub struct PtyProcessInfo {
system: RwLock<System>,
refresh_kind: ProcessRefreshKind,
pid_getter: ProcessIdGetter,
pub current: RwLock<Option<ProcessInfo>>,
task: Mutex<Option<Task<()>>>,
}
impl PtyProcessInfo {
pub fn new(pty: &Pty) -> PtyProcessInfo {
let process_refresh_kind = ProcessRefreshKind::nothing()
.with_cmd(UpdateKind::Always)
.with_cwd(UpdateKind::Always)
.with_exe(UpdateKind::Always);
let refresh_kind = RefreshKind::nothing().with_processes(process_refresh_kind);
let system = System::new_with_specifics(refresh_kind);
PtyProcessInfo {
system: RwLock::new(system),
refresh_kind: process_refresh_kind,
pid_getter: ProcessIdGetter::new(pty),
current: RwLock::new(None),
task: Mutex::new(None),
}
}
pub fn pid_getter(&self) -> &ProcessIdGetter {
&self.pid_getter
}
fn refresh(&self) -> Option<MappedRwLockReadGuard<'_, Process>> {
let pid = self.pid_getter.pid()?;
if self.system.write().refresh_processes_specifics(
sysinfo::ProcessesToUpdate::Some(&[pid]),
true,
self.refresh_kind,
) == 1
{
RwLockReadGuard::try_map(self.system.read(), |system| system.process(pid)).ok()
} else {
None
}
}
fn get_child(&self) -> Option<MappedRwLockReadGuard<'_, Process>> {
let pid = self.pid_getter.fallback_pid();
RwLockReadGuard::try_map(self.system.read(), |system| system.process(pid)).ok()
}
#[cfg(unix)]
pub(crate) fn kill_current_process(&self) -> bool {
let Some(pid) = self.pid_getter.pid() else {
return false;
};
unsafe { libc::killpg(pid.as_u32() as i32, libc::SIGKILL) == 0 }
}
#[cfg(not(unix))]
pub(crate) fn kill_current_process(&self) -> bool {
self.refresh().is_some_and(|process| process.kill())
}
pub(crate) fn kill_child_process(&self) -> bool {
self.get_child().is_some_and(|process| process.kill())
}
#[cfg(unix)]
pub(crate) fn terminate_child_process(&self) -> bool {
let pid = self.pid_getter.fallback_pid();
unsafe { libc::killpg(pid.as_u32() as i32, libc::SIGTERM) == 0 }
}
#[cfg(not(unix))]
pub(crate) fn terminate_child_process(&self) -> bool {
false
}
fn load(&self) -> Option<ProcessInfo> {
let process = self.refresh()?;
let cwd = process.cwd().map_or(PathBuf::new(), |p| p.to_owned());
let info = ProcessInfo {
name: process.name().to_str()?.to_owned(),
cwd,
argv: process
.cmd()
.iter()
.filter_map(|s| s.to_str().map(ToOwned::to_owned))
.collect(),
};
*self.current.write() = Some(info.clone());
Some(info)
}
/// Updates the cached process info, emitting a [`Event::TitleChanged`] event if the Zed-relevant info has changed
pub fn emit_title_changed_if_changed(self: &Arc<Self>, cx: &mut Context<'_, Terminal>) {
if self.task.lock().is_some() {
return;
}
let this = self.clone();
let has_changed = cx.background_executor().spawn(async move {
let current = this.load();
let has_changed = match (this.current.read().as_ref(), current.as_ref()) {
(None, None) => false,
(Some(prev), Some(now)) => prev.cwd != now.cwd || prev.name != now.name,
_ => true,
};
if has_changed {
*this.current.write() = current;
}
has_changed
});
let this = Arc::downgrade(self);
*self.task.lock() = Some(cx.spawn(async move |term, cx| {
if has_changed.await {
term.update(cx, |_, cx| cx.emit(Event::TitleChanged)).ok();
}
if let Some(this) = this.upgrade() {
this.task.lock().take();
}
}));
}
pub fn pid(&self) -> Option<Pid> {
self.pid_getter.pid()
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,185 @@
use alacritty_terminal::vte::ansi::{
CursorShape as AlacCursorShape, CursorStyle as AlacCursorStyle,
};
use collections::HashMap;
use gpui::{FontFallbacks, FontFeatures, FontWeight, Pixels, px};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
pub use settings::AlternateScroll;
use settings::{
IntoGpui, PathHyperlinkRegex, RegisterSetting, ShowScrollbar, TerminalBell, TerminalBlink,
TerminalDockPosition, TerminalLineHeight, VenvSettings, WorkingDirectory,
merge_from::MergeFrom,
};
use task::Shell;
use theme_settings::FontFamilyName;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct Toolbar {
pub breadcrumbs: bool,
}
#[derive(Clone, Debug, Deserialize, RegisterSetting)]
pub struct TerminalSettings {
pub shell: Shell,
pub working_directory: WorkingDirectory,
pub font_size: Option<Pixels>, // todo(settings_refactor) can be non-optional...
pub font_family: Option<FontFamilyName>,
pub font_fallbacks: Option<FontFallbacks>,
pub font_features: Option<FontFeatures>,
pub font_weight: Option<FontWeight>,
pub line_height: TerminalLineHeight,
pub env: HashMap<String, String>,
pub cursor_shape: CursorShape,
pub blinking: TerminalBlink,
pub alternate_scroll: AlternateScroll,
pub option_as_meta: bool,
pub copy_on_select: bool,
pub keep_selection_on_copy: bool,
pub button: bool,
pub dock: TerminalDockPosition,
pub flexible: bool,
pub default_width: Pixels,
pub default_height: Pixels,
pub detect_venv: VenvSettings,
pub max_scroll_history_lines: Option<usize>,
pub scroll_multiplier: f32,
pub toolbar: Toolbar,
pub scrollbar: ScrollbarSettings,
pub minimum_contrast: f32,
pub path_hyperlink_regexes: Vec<String>,
pub path_hyperlink_timeout_ms: u64,
pub show_count_badge: bool,
pub bell: TerminalBell,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct ScrollbarSettings {
/// When to show the scrollbar in the terminal.
///
/// Default: inherits editor scrollbar settings
pub show: Option<ShowScrollbar>,
}
fn settings_shell_to_task_shell(shell: settings::Shell) -> Shell {
match shell {
settings::Shell::System => Shell::System,
settings::Shell::Program(program) => Shell::Program(program),
settings::Shell::WithArguments {
program,
args,
title_override,
} => Shell::WithArguments {
program,
args,
title_override,
},
}
}
impl settings::Settings for TerminalSettings {
fn from_settings(content: &settings::SettingsContent) -> Self {
let user_content = content.terminal.clone().unwrap();
// Note: we allow a subset of "terminal" settings in the project files.
let mut project_content = user_content.project.clone();
project_content.merge_from_option(content.project.terminal.as_ref());
TerminalSettings {
shell: settings_shell_to_task_shell(project_content.shell.unwrap()),
working_directory: project_content.working_directory.unwrap(),
font_size: user_content.font_size.map(|s| s.into_gpui()),
font_family: user_content.font_family,
font_fallbacks: user_content.font_fallbacks.map(|fallbacks| {
FontFallbacks::from_fonts(
fallbacks
.into_iter()
.map(|family| family.0.to_string())
.collect(),
)
}),
font_features: user_content.font_features.map(|f| f.into_gpui()),
font_weight: user_content.font_weight.map(|w| w.into_gpui()),
line_height: user_content.line_height.unwrap(),
env: project_content.env.unwrap(),
cursor_shape: user_content.cursor_shape.unwrap().into(),
blinking: user_content.blinking.unwrap(),
alternate_scroll: user_content.alternate_scroll.unwrap(),
option_as_meta: user_content.option_as_meta.unwrap(),
copy_on_select: user_content.copy_on_select.unwrap(),
keep_selection_on_copy: user_content.keep_selection_on_copy.unwrap(),
button: user_content.button.unwrap(),
dock: user_content.dock.unwrap(),
default_width: px(user_content.default_width.unwrap()),
default_height: px(user_content.default_height.unwrap()),
flexible: user_content.flexible.unwrap(),
detect_venv: project_content.detect_venv.unwrap(),
scroll_multiplier: user_content.scroll_multiplier.unwrap(),
max_scroll_history_lines: user_content.max_scroll_history_lines,
toolbar: Toolbar {
breadcrumbs: user_content.toolbar.unwrap().breadcrumbs.unwrap(),
},
scrollbar: ScrollbarSettings {
show: user_content.scrollbar.unwrap().show,
},
minimum_contrast: user_content.minimum_contrast.unwrap(),
path_hyperlink_regexes: project_content
.path_hyperlink_regexes
.unwrap()
.into_iter()
.map(|regex| match regex {
PathHyperlinkRegex::SingleLine(regex) => regex,
PathHyperlinkRegex::MultiLine(regex) => regex.join("\n"),
})
.collect(),
path_hyperlink_timeout_ms: project_content.path_hyperlink_timeout_ms.unwrap(),
show_count_badge: user_content.show_count_badge.unwrap(),
bell: user_content.bell.unwrap(),
}
}
}
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum CursorShape {
/// Cursor is a block like `█`.
#[default]
Block,
/// Cursor is an underscore like `_`.
Underline,
/// Cursor is a vertical bar like `⎸`.
Bar,
/// Cursor is a hollow box like `▯`.
Hollow,
}
impl From<settings::CursorShapeContent> for CursorShape {
fn from(value: settings::CursorShapeContent) -> Self {
match value {
settings::CursorShapeContent::Block => CursorShape::Block,
settings::CursorShapeContent::Underline => CursorShape::Underline,
settings::CursorShapeContent::Bar => CursorShape::Bar,
settings::CursorShapeContent::Hollow => CursorShape::Hollow,
}
}
}
impl From<CursorShape> for AlacCursorShape {
fn from(value: CursorShape) -> Self {
match value {
CursorShape::Block => AlacCursorShape::Block,
CursorShape::Underline => AlacCursorShape::Underline,
CursorShape::Bar => AlacCursorShape::Beam,
CursorShape::Hollow => AlacCursorShape::HollowBlock,
}
}
}
impl From<CursorShape> for AlacCursorStyle {
fn from(value: CursorShape) -> Self {
AlacCursorStyle {
shape: value.into(),
blinking: false,
}
}
}