T09: the tap holds one mouse tracking and one format as xterm does, any reset ends it; every reported button keeps its release, motion reports the first held; URLs keep combining marks; selections log start, run and end; shot hands real buttons to the view and shows its window without activating
This commit is contained in:
10 files changed
+311
-206
No files matched your search
+3
-1
@@ -38,7 +38,9 @@ impl TerminalView {
|
||||
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);
|
||||
term.selection = None;
|
||||
if term.selection.take().is_some() {
|
||||
log::debug!("terminal {}: input clears the selection", self.pty.pid);
|
||||
}
|
||||
drop(term);
|
||||
self.pty.write(bytes);
|
||||
cx.notify();
|
||||
|
||||
+1
-1
@@ -51,7 +51,7 @@ pub fn encode(key: &Key, app_cursor: bool, kitty: Kitty) -> Option<Vec<u8>> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Pasted text as the program gets it; bracketed text loses ESC and Ctrl-C, as alacritty's does.
|
||||
/// Pasted text for the program; bracketed text loses ESC and Ctrl-C, as in alacritty.
|
||||
pub fn paste(text: &str, bracketed: bool) -> Vec<u8> {
|
||||
if bracketed {
|
||||
let text = text.replace(['\x1b', '\x03'], "");
|
||||
|
||||
+8
-1
@@ -4,7 +4,7 @@ use alacritty_terminal::{
|
||||
};
|
||||
use ely_gpui_component::terminal::{Target, links};
|
||||
|
||||
/// The link at `point`: the program's OSC 8 target, else a URL in its line, wrapped rows joined.
|
||||
/// The OSC 8 target at `point`, else a URL in its wrapped line.
|
||||
pub fn link_at<T>(term: &Term<T>, mut point: Point) -> Option<String> {
|
||||
let grid = term.grid();
|
||||
if grid[point].flags.contains(Flags::WIDE_CHAR_SPACER) {
|
||||
@@ -23,6 +23,7 @@ pub fn link_at<T>(term: &Term<T>, mut point: Point) -> Option<String> {
|
||||
let spacer = Flags::WIDE_CHAR_SPACER | Flags::LEADING_WIDE_CHAR_SPACER;
|
||||
if !grid[cell].flags.intersects(spacer) {
|
||||
text.push(grid[cell].c);
|
||||
text.extend(grid[cell].zerowidth().into_iter().flatten());
|
||||
}
|
||||
if cell == end {
|
||||
break;
|
||||
@@ -70,6 +71,12 @@ mod tests {
|
||||
assert_eq!(link(text, 0, 11), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_url_keeps_its_combining_marks() {
|
||||
let text = "https://e.test/cafe\u{301}/x";
|
||||
assert_eq!(link(text, 1, 1).as_deref(), Some(text));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_url_runs_across_wrapped_rows_and_wide_cells() {
|
||||
let text = "\u{4e2d} https://ely.dev/abc";
|
||||
|
||||
+91
-69
@@ -1,5 +1,3 @@
|
||||
use alacritty_terminal::term::TermMode;
|
||||
|
||||
/// A button as reports name it; the wheel's two turns count as buttons.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Button {
|
||||
@@ -32,8 +30,9 @@ pub struct Report {
|
||||
}
|
||||
|
||||
/// The mouse events a program asked for.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum Tracking {
|
||||
#[default]
|
||||
Off,
|
||||
/// Mode 9: presses alone, no modifiers.
|
||||
X10,
|
||||
@@ -45,30 +44,51 @@ pub enum Tracking {
|
||||
Any,
|
||||
}
|
||||
|
||||
impl Tracking {
|
||||
/// The tracking `mode` sets; `x10` is mode 9, which the tap follows.
|
||||
pub fn new(mode: TermMode, x10: bool) -> Self {
|
||||
if x10 {
|
||||
Self::X10
|
||||
} else if mode.contains(TermMode::MOUSE_MOTION) {
|
||||
Self::Any
|
||||
} else if mode.contains(TermMode::MOUSE_DRAG) {
|
||||
Self::Button
|
||||
} else if mode.contains(TermMode::MOUSE_REPORT_CLICK) {
|
||||
Self::Normal
|
||||
} else {
|
||||
Self::Off
|
||||
}
|
||||
/// The bytes reports go in: xterm's default, mode 1005 or mode 1006.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum Format {
|
||||
#[default]
|
||||
X10,
|
||||
Utf8,
|
||||
Sgr,
|
||||
}
|
||||
|
||||
/// How a program asked for the mouse.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct MouseMode {
|
||||
pub tracking: Tracking,
|
||||
pub format: Format,
|
||||
}
|
||||
|
||||
impl MouseMode {
|
||||
/// Follows `CSI ? mode h` or `l`; as in xterm, a reset ends the tracking or format.
|
||||
pub fn apply(&mut self, mode: u32, set: bool) {
|
||||
let tracking = match mode {
|
||||
9 => Tracking::X10,
|
||||
1000 => Tracking::Normal,
|
||||
1002 => Tracking::Button,
|
||||
1003 => Tracking::Any,
|
||||
1005 | 1006 => {
|
||||
self.format = match (set, mode) {
|
||||
(false, _) => Format::X10,
|
||||
(true, 1005) => Format::Utf8,
|
||||
(true, _) => Format::Sgr,
|
||||
};
|
||||
return;
|
||||
}
|
||||
_ => return,
|
||||
};
|
||||
self.tracking = if set { tracking } else { Tracking::Off };
|
||||
}
|
||||
}
|
||||
|
||||
/// The bytes `report` sends; none when the program did not ask for it or its format cannot hold the cell.
|
||||
pub fn encode(report: &Report, tracking: Tracking, mode: TermMode) -> Option<Vec<u8>> {
|
||||
/// The bytes `report` sends; none when unasked for or past what the format holds.
|
||||
pub fn encode(report: &Report, mode: MouseMode) -> Option<Vec<u8>> {
|
||||
let clicked = matches!(
|
||||
report.button,
|
||||
Some(Button::Left | Button::Middle | Button::Right)
|
||||
);
|
||||
let wanted = match tracking {
|
||||
let wanted = match mode.tracking {
|
||||
Tracking::Off => false,
|
||||
Tracking::X10 => report.gesture == Gesture::Press && clicked,
|
||||
Tracking::Normal => report.gesture != Gesture::Motion,
|
||||
@@ -78,7 +98,7 @@ pub fn encode(report: &Report, tracking: Tracking, mode: TermMode) -> Option<Vec
|
||||
if !wanted {
|
||||
return None;
|
||||
}
|
||||
let sgr = mode.contains(TermMode::SGR_MOUSE);
|
||||
let sgr = mode.format == Format::Sgr;
|
||||
let mut code = match report.button {
|
||||
None => 3,
|
||||
Some(_) if report.gesture == Gesture::Release && !sgr => 3,
|
||||
@@ -88,7 +108,7 @@ pub fn encode(report: &Report, tracking: Tracking, mode: TermMode) -> Option<Vec
|
||||
Some(Button::WheelUp) => 64,
|
||||
Some(Button::WheelDown) => 65,
|
||||
};
|
||||
if tracking != Tracking::X10 {
|
||||
if mode.tracking != Tracking::X10 {
|
||||
code +=
|
||||
4 * u32::from(report.shift) + 8 * u32::from(report.alt) + 16 * u32::from(report.ctrl);
|
||||
}
|
||||
@@ -104,7 +124,7 @@ pub fn encode(report: &Report, tracking: Tracking, mode: TermMode) -> Option<Vec
|
||||
};
|
||||
return Some(format!("\x1b[<{code};{column};{row}{end}").into_bytes());
|
||||
}
|
||||
let utf8 = mode.contains(TermMode::UTF8_MOUSE);
|
||||
let utf8 = mode.format == Format::Utf8;
|
||||
let most = if utf8 { 0x7FF - 32 } else { 0xFF - 32 };
|
||||
if column > most || row > most {
|
||||
log::debug!("mouse: cell {column},{row} is past what the format holds");
|
||||
@@ -139,102 +159,104 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn sent(report: Report, tracking: Tracking, mode: TermMode) -> Option<Vec<u8>> {
|
||||
encode(&report, tracking, mode)
|
||||
fn mode(tracking: Tracking, format: Format) -> MouseMode {
|
||||
MouseMode { tracking, format }
|
||||
}
|
||||
|
||||
const LEFT: Option<Button> = Some(Button::Left);
|
||||
|
||||
#[test]
|
||||
fn one_tracking_and_one_format_hold_and_a_reset_ends_them() {
|
||||
let mut held = MouseMode::default();
|
||||
for (code, set) in [(1000, true), (9, true), (9, false), (1006, true)] {
|
||||
held.apply(code, set);
|
||||
}
|
||||
assert_eq!(held, mode(Tracking::Off, Format::Sgr), "9 replaced 1000");
|
||||
held.apply(1002, true);
|
||||
held.apply(1003, false);
|
||||
assert_eq!(held.tracking, Tracking::Off, "any reset ends tracking");
|
||||
held.apply(1003, true);
|
||||
held.apply(1005, true);
|
||||
assert_eq!(
|
||||
held,
|
||||
mode(Tracking::Any, Format::Utf8),
|
||||
"the last format holds"
|
||||
);
|
||||
held.apply(1006, false);
|
||||
held.apply(25, true);
|
||||
assert_eq!(held, mode(Tracking::Any, Format::X10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normal_tracking_sends_presses_and_releases_as_x10_bytes() {
|
||||
let mode = TermMode::MOUSE_REPORT_CLICK;
|
||||
let tracking = Tracking::new(mode, false);
|
||||
assert_eq!(tracking, Tracking::Normal);
|
||||
let normal = mode(Tracking::Normal, Format::X10);
|
||||
let press = report(LEFT, Gesture::Press, (0, 0));
|
||||
assert_eq!(sent(press, tracking, mode), Some(b"\x1b[M !!".to_vec()));
|
||||
assert_eq!(encode(&press, normal), Some(b"\x1b[M !!".to_vec()));
|
||||
let release = report(LEFT, Gesture::Release, (3, 2));
|
||||
assert_eq!(sent(release, tracking, mode), Some(b"\x1b[M#$#".to_vec()));
|
||||
assert_eq!(encode(&release, normal), Some(b"\x1b[M#$#".to_vec()));
|
||||
let motion = report(LEFT, Gesture::Motion, (1, 1));
|
||||
assert_eq!(sent(motion, tracking, mode), None);
|
||||
assert_eq!(encode(&motion, normal), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sgr_names_the_released_button_and_the_modifiers() {
|
||||
let mode = TermMode::MOUSE_REPORT_CLICK | TermMode::SGR_MOUSE;
|
||||
let sgr = mode(Tracking::Normal, Format::Sgr);
|
||||
let press = Report {
|
||||
shift: true,
|
||||
ctrl: true,
|
||||
..report(Some(Button::Right), Gesture::Press, (4, 2))
|
||||
};
|
||||
assert_eq!(
|
||||
sent(press, Tracking::Normal, mode),
|
||||
Some(b"\x1b[<22;5;3M".to_vec())
|
||||
);
|
||||
assert_eq!(encode(&press, sgr), Some(b"\x1b[<22;5;3M".to_vec()));
|
||||
let release = report(Some(Button::Middle), Gesture::Release, (300, 9));
|
||||
assert_eq!(
|
||||
sent(release, Tracking::Normal, mode),
|
||||
Some(b"\x1b[<1;301;10m".to_vec())
|
||||
);
|
||||
assert_eq!(encode(&release, sgr), Some(b"\x1b[<1;301;10m".to_vec()));
|
||||
let alt = Report {
|
||||
alt: true,
|
||||
..report(Some(Button::WheelDown), Gesture::Press, (0, 0))
|
||||
};
|
||||
assert_eq!(
|
||||
sent(alt, Tracking::Normal, mode),
|
||||
Some(b"\x1b[<73;1;1M".to_vec())
|
||||
);
|
||||
assert_eq!(encode(&alt, sgr), Some(b"\x1b[<73;1;1M".to_vec()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn motion_needs_button_or_any_tracking() {
|
||||
let mode = TermMode::SGR_MOUSE;
|
||||
let held = report(LEFT, Gesture::Motion, (1, 0));
|
||||
let free = report(None, Gesture::Motion, (1, 0));
|
||||
assert_eq!(Tracking::new(TermMode::MOUSE_DRAG, false), Tracking::Button);
|
||||
assert_eq!(
|
||||
sent(held, Tracking::Button, mode),
|
||||
Some(b"\x1b[<32;2;1M".to_vec())
|
||||
);
|
||||
assert_eq!(sent(free, Tracking::Button, mode), None);
|
||||
assert_eq!(Tracking::new(TermMode::MOUSE_MOTION, false), Tracking::Any);
|
||||
assert_eq!(
|
||||
sent(free, Tracking::Any, mode),
|
||||
Some(b"\x1b[<35;2;1M".to_vec())
|
||||
);
|
||||
let button = mode(Tracking::Button, Format::Sgr);
|
||||
assert_eq!(encode(&held, button), Some(b"\x1b[<32;2;1M".to_vec()));
|
||||
assert_eq!(encode(&free, button), None);
|
||||
let any = mode(Tracking::Any, Format::Sgr);
|
||||
assert_eq!(encode(&free, any), Some(b"\x1b[<35;2;1M".to_vec()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn x10_tracking_sends_presses_alone_without_modifiers() {
|
||||
let mode = TermMode::MOUSE_REPORT_CLICK;
|
||||
let tracking = Tracking::new(mode, true);
|
||||
assert_eq!(tracking, Tracking::X10);
|
||||
let x10 = mode(Tracking::X10, Format::X10);
|
||||
let press = Report {
|
||||
shift: true,
|
||||
..report(LEFT, Gesture::Press, (0, 0))
|
||||
};
|
||||
assert_eq!(sent(press, tracking, mode), Some(b"\x1b[M !!".to_vec()));
|
||||
assert_eq!(encode(&press, x10), Some(b"\x1b[M !!".to_vec()));
|
||||
let release = report(LEFT, Gesture::Release, (0, 0));
|
||||
assert_eq!(sent(release, tracking, mode), None);
|
||||
assert_eq!(encode(&release, x10), None);
|
||||
let wheel = report(Some(Button::WheelUp), Gesture::Press, (0, 0));
|
||||
assert_eq!(sent(wheel, tracking, mode), None);
|
||||
assert_eq!(Tracking::new(TermMode::empty(), false), Tracking::Off);
|
||||
assert_eq!(encode(&wheel, x10), None);
|
||||
assert_eq!(encode(&press, MouseMode::default()), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cells_past_the_format_go_utf8_or_nowhere() {
|
||||
let mode = TermMode::MOUSE_REPORT_CLICK;
|
||||
let normal = mode(Tracking::Normal, Format::X10);
|
||||
let edge = report(LEFT, Gesture::Press, (222, 0));
|
||||
assert_eq!(
|
||||
sent(edge, Tracking::Normal, mode),
|
||||
encode(&edge, normal),
|
||||
Some(vec![0x1b, b'[', b'M', 32, 255, 33])
|
||||
);
|
||||
let past = report(LEFT, Gesture::Press, (223, 0));
|
||||
assert_eq!(sent(past, Tracking::Normal, mode), None);
|
||||
let utf8 = mode | TermMode::UTF8_MOUSE;
|
||||
assert_eq!(encode(&past, normal), None);
|
||||
let utf8 = mode(Tracking::Normal, Format::Utf8);
|
||||
let mut wide = b"\x1b[M ".to_vec();
|
||||
wide.extend("\u{100}!".as_bytes());
|
||||
assert_eq!(sent(past, Tracking::Normal, utf8), Some(wide));
|
||||
assert_eq!(encode(&past, utf8), Some(wide));
|
||||
let far = report(LEFT, Gesture::Press, (2015, 0));
|
||||
assert_eq!(sent(far, Tracking::Normal, utf8), None);
|
||||
assert_eq!(encode(&far, utf8), None);
|
||||
}
|
||||
}
|
||||
+40
-17
@@ -12,7 +12,7 @@ use gpui::{
|
||||
use super::{
|
||||
keys,
|
||||
links::link_at,
|
||||
mouse::{self, Button, Gesture, Report, Tracking},
|
||||
mouse::{self, Button, Gesture, MouseMode, Report, Tracking},
|
||||
view::TerminalView,
|
||||
};
|
||||
use crate::settings::SettingsStore;
|
||||
@@ -28,8 +28,8 @@ pub struct Pointer {
|
||||
pub cell: Size<Pixels>,
|
||||
/// The left button is down and drawing a selection.
|
||||
selecting: bool,
|
||||
/// The button whose press went to the program.
|
||||
pressed: Option<Button>,
|
||||
/// Buttons whose press went to the program.
|
||||
pressed: Vec<Button>,
|
||||
/// The cell of the last report; motion inside it sends nothing.
|
||||
last: Option<(usize, usize)>,
|
||||
/// Wheel travel short of a line.
|
||||
@@ -66,9 +66,10 @@ impl TerminalView {
|
||||
/// What the program tracks; none while Shift holds the mouse for selecting.
|
||||
fn tracking(&self, modifiers: &Modifiers) -> Tracking {
|
||||
if modifiers.shift {
|
||||
return Tracking::Off;
|
||||
Tracking::Off
|
||||
} else {
|
||||
self.mouse.tracking
|
||||
}
|
||||
Tracking::new(*self.pty.term.lock().mode(), self.x10)
|
||||
}
|
||||
|
||||
fn report(
|
||||
@@ -87,8 +88,8 @@ impl TerminalView {
|
||||
alt: modifiers.alt,
|
||||
ctrl: modifiers.control,
|
||||
};
|
||||
let mode = *self.pty.term.lock().mode();
|
||||
let Some(bytes) = mouse::encode(&report, tracking, mode) else {
|
||||
let format = self.mouse.format;
|
||||
let Some(bytes) = mouse::encode(&report, MouseMode { tracking, format }) else {
|
||||
return false;
|
||||
};
|
||||
log::trace!("terminal {}: {report:?} sends {bytes:?}", self.pty.pid);
|
||||
@@ -129,27 +130,33 @@ impl TerminalView {
|
||||
cell,
|
||||
&event.modifiers,
|
||||
tracking,
|
||||
) {
|
||||
self.pointer.pressed = Some(pressed);
|
||||
) && !self.pointer.pressed.contains(&pressed)
|
||||
{
|
||||
self.pointer.pressed.push(pressed);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if event.button != MouseButton::Left {
|
||||
return;
|
||||
}
|
||||
let pid = self.pty.pid;
|
||||
let mut term = self.pty.term.lock();
|
||||
match (
|
||||
term.selection.as_mut(),
|
||||
event.modifiers.shift,
|
||||
event.click_count,
|
||||
) {
|
||||
(Some(selection), true, 1) => selection.update(point, side),
|
||||
(Some(selection), true, 1) => {
|
||||
log::debug!("terminal {pid}: the selection extends to {point:?}");
|
||||
selection.update(point, side);
|
||||
}
|
||||
_ => {
|
||||
let kind = match event.click_count {
|
||||
2 => SelectionType::Semantic,
|
||||
3.. => SelectionType::Lines,
|
||||
_ => SelectionType::Simple,
|
||||
};
|
||||
log::debug!("terminal {pid}: a {kind:?} selection starts at {point:?}");
|
||||
term.selection = Some(Selection::new(kind, point, side));
|
||||
}
|
||||
}
|
||||
@@ -167,6 +174,7 @@ impl TerminalView {
|
||||
let (cell, point, side) = self.cell_at(event.position);
|
||||
if self.pointer.selecting && event.pressed_button == Some(MouseButton::Left) {
|
||||
if let Some(selection) = self.pty.term.lock().selection.as_mut() {
|
||||
log::trace!("terminal {}: the selection runs to {point:?}", self.pty.pid);
|
||||
selection.update(point, side);
|
||||
}
|
||||
cx.notify();
|
||||
@@ -174,7 +182,9 @@ impl TerminalView {
|
||||
}
|
||||
let tracking = self.tracking(&event.modifiers);
|
||||
if tracking != Tracking::Off && self.pointer.last != Some(cell) {
|
||||
let held = self.pointer.pressed;
|
||||
let held = [Button::Left, Button::Right, Button::Middle]
|
||||
.into_iter()
|
||||
.find(|button| self.pointer.pressed.contains(button));
|
||||
self.report(held, Gesture::Motion, cell, &event.modifiers, tracking);
|
||||
}
|
||||
}
|
||||
@@ -187,16 +197,29 @@ impl TerminalView {
|
||||
) {
|
||||
if event.button == MouseButton::Left && std::mem::take(&mut self.pointer.selecting) {
|
||||
let selected = self.selected();
|
||||
let count = selected.as_ref().map_or(0, |text| text.chars().count());
|
||||
log::debug!(
|
||||
"terminal {}: the selection ends on {count} characters",
|
||||
self.pty.pid
|
||||
);
|
||||
if let Some(text) = selected.filter(|_| SettingsStore::get(cx).copy_on_select) {
|
||||
self.copy_text(text, cx);
|
||||
}
|
||||
}
|
||||
let released = button(event.button);
|
||||
if released.is_some() && self.pointer.pressed == released {
|
||||
self.pointer.pressed = None;
|
||||
let Some(released) = button(event.button) else {
|
||||
return;
|
||||
};
|
||||
if self.pointer.pressed.contains(&released) {
|
||||
self.pointer.pressed.retain(|held| *held != released);
|
||||
let (cell, ..) = self.cell_at(event.position);
|
||||
let tracking = Tracking::new(*self.pty.term.lock().mode(), self.x10);
|
||||
self.report(released, Gesture::Release, cell, &event.modifiers, tracking);
|
||||
let tracking = self.mouse.tracking;
|
||||
self.report(
|
||||
Some(released),
|
||||
Gesture::Release,
|
||||
cell,
|
||||
&event.modifiers,
|
||||
tracking,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,7 +270,7 @@ impl TerminalView {
|
||||
}
|
||||
}
|
||||
|
||||
/// The selected text, without the newline alacritty ends a whole line with, as Ghostty copies it.
|
||||
/// The selected text without alacritty's final newline, as Ghostty copies it.
|
||||
fn selected(&self) -> Option<String> {
|
||||
let mut text = self.pty.term.lock().selection_to_string()?;
|
||||
if text.ends_with('\n') {
|
||||
|
||||
+4
-5
@@ -11,16 +11,15 @@ use alacritty_terminal::{
|
||||
};
|
||||
use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender, unbounded};
|
||||
|
||||
use super::tap::Tapped;
|
||||
use super::{mouse::MouseMode, tap::Tapped};
|
||||
use crate::settings::{CursorStyle, Settings};
|
||||
|
||||
/// What a session hears from its pty: the grid's events, the folder the shell reports, and mode 9.
|
||||
/// What a session hears from its pty.
|
||||
#[derive(Debug)]
|
||||
pub enum PtyEvent {
|
||||
Term(Event),
|
||||
Folder(PathBuf),
|
||||
/// X10 mouse reporting turned on or off.
|
||||
X10(bool),
|
||||
Mouse(MouseMode),
|
||||
}
|
||||
|
||||
/// Hands the grid's events to the session; `pid` names the program in logs.
|
||||
@@ -250,7 +249,7 @@ mod tests {
|
||||
PtyEvent::Folder(folder) => folders.push(folder),
|
||||
PtyEvent::Term(Event::ChildExit(status)) => success = status.success(),
|
||||
PtyEvent::Term(Event::Exit) => break,
|
||||
PtyEvent::Term(_) | PtyEvent::X10(_) => {}
|
||||
PtyEvent::Term(_) | PtyEvent::Mouse(_) => {}
|
||||
}
|
||||
}
|
||||
(folders, success)
|
||||
|
||||
+34
-36
@@ -7,16 +7,16 @@ use alacritty_terminal::{
|
||||
use futures::channel::mpsc::UnboundedSender;
|
||||
use polling::{Event, PollMode, Poller};
|
||||
|
||||
use super::pty::PtyEvent;
|
||||
use super::{mouse::MouseMode, pty::PtyEvent};
|
||||
|
||||
/// The longest OSC body kept; longer ones are dropped whole.
|
||||
const MOST: usize = 4096;
|
||||
|
||||
/// A pty whose reader hands each byte to the OSC 7 and mode 9 scanners before the parser sees it.
|
||||
/// A pty whose reader shows each byte to its scanners before the parser.
|
||||
pub(crate) struct Tapped<P> {
|
||||
pub inner: P,
|
||||
scanner: Osc7,
|
||||
x10: Mode9,
|
||||
mouse: MouseModes,
|
||||
events: UnboundedSender<PtyEvent>,
|
||||
pid: u32,
|
||||
last: Option<PathBuf>,
|
||||
@@ -27,7 +27,7 @@ impl<P> Tapped<P> {
|
||||
Self {
|
||||
inner,
|
||||
scanner: Osc7::default(),
|
||||
x10: Mode9::default(),
|
||||
mouse: MouseModes::default(),
|
||||
events,
|
||||
pid,
|
||||
last: None,
|
||||
@@ -53,12 +53,9 @@ impl<P: EventedReadWrite> io::Read for Tapped<P> {
|
||||
log::debug!("pty {pid}: a folder report after its session closed");
|
||||
}
|
||||
});
|
||||
if let Some(on) = self.x10.feed(&buf[..count]) {
|
||||
log::info!(
|
||||
"pty {pid}: X10 mouse reporting {}",
|
||||
if on { "on" } else { "off" }
|
||||
);
|
||||
if events.unbounded_send(PtyEvent::X10(on)).is_err() {
|
||||
if let Some(mode) = self.mouse.feed(&buf[..count]) {
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -197,18 +194,18 @@ enum Csi {
|
||||
Private,
|
||||
}
|
||||
|
||||
/// Follows `CSI ? 9 h` and `l`, X10 mouse reporting, across reads: alacritty's parser drops mode 9.
|
||||
/// The mouse modes as xterm holds them; alacritty drops 9 and splits the rest.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Mode9 {
|
||||
pub(crate) struct MouseModes {
|
||||
state: Csi,
|
||||
params: Vec<u32>,
|
||||
on: bool,
|
||||
mode: MouseMode,
|
||||
}
|
||||
|
||||
impl Mode9 {
|
||||
impl MouseModes {
|
||||
/// The mode after `bytes`, when they changed it.
|
||||
pub fn feed(&mut self, bytes: &[u8]) -> Option<bool> {
|
||||
let was = self.on;
|
||||
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 {
|
||||
@@ -225,7 +222,7 @@ impl Mode9 {
|
||||
(_, 0x1b) => Csi::Escape,
|
||||
(Csi::Escape, b'[') => Csi::Bracket,
|
||||
(Csi::Escape, b'c') => {
|
||||
self.on = false;
|
||||
self.mode = MouseMode::default();
|
||||
Csi::Ground
|
||||
}
|
||||
(Csi::Bracket, b'?') => {
|
||||
@@ -244,20 +241,15 @@ impl Mode9 {
|
||||
Csi::Private
|
||||
}
|
||||
(Csi::Private, b'h' | b'l') => {
|
||||
let set = byte == b'h';
|
||||
for mode in &self.params {
|
||||
match mode {
|
||||
9 => self.on = set,
|
||||
1000 | 1002 | 1003 if set => self.on = false,
|
||||
_ => {}
|
||||
}
|
||||
for &mode in &self.params {
|
||||
self.mode.apply(mode, byte == b'h');
|
||||
}
|
||||
Csi::Ground
|
||||
}
|
||||
_ => Csi::Ground,
|
||||
};
|
||||
}
|
||||
(self.on != was).then_some(self.on)
|
||||
(self.mode != was).then_some(self.mode)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,17 +351,23 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mode_9_follows_its_sets_resets_and_the_modes_that_replace_it() {
|
||||
let mut mode = Mode9::default();
|
||||
assert_eq!(mode.feed(b"a\x1b[?"), None);
|
||||
assert_eq!(mode.feed(b"1006;9h"), Some(true));
|
||||
assert_eq!(mode.feed(b"\x1b[9m\x1b[?25l\x1b[?9"), None);
|
||||
assert_eq!(mode.feed(b"l"), Some(false));
|
||||
assert_eq!(mode.feed(b"\x1b[?9h\x1b[?1000h"), None, "1000 replaced it");
|
||||
assert_eq!(mode.feed(b"\x1b[?9h"), Some(true));
|
||||
assert_eq!(mode.feed(b"\x1b[?1000l"), None);
|
||||
assert_eq!(mode.feed(b"\x1bc"), Some(false), "a reset ends it");
|
||||
assert_eq!(mode.feed(b"\x1b[?09;\x1b[?9h"), Some(true));
|
||||
fn mouse_modes_follow_private_modes_in_order_across_reads() {
|
||||
use crate::term::mouse::{Format, Tracking};
|
||||
let mut modes = MouseModes::default();
|
||||
assert_eq!(modes.feed(b"a\x1b[?"), None);
|
||||
assert!(modes.feed(b"1006;9h").is_some());
|
||||
assert_eq!(modes.mode.format, Format::Sgr);
|
||||
assert_eq!(modes.mode.tracking, Tracking::X10);
|
||||
assert_eq!(modes.feed(b"\x1b[9m\x1b[?25l\x1b[?9"), None);
|
||||
assert!(modes.feed(b"l").is_some());
|
||||
assert_eq!(modes.mode.tracking, Tracking::Off);
|
||||
modes.feed(b"\x1b[?1000h\x1b[?9h\x1b[?9l");
|
||||
assert_eq!(modes.mode.tracking, Tracking::Off, "9 replaced 1000");
|
||||
modes.feed(b"\x1b[?1002h");
|
||||
let reset = modes.feed(b"\x1bc");
|
||||
assert_eq!(reset, Some(MouseMode::default()), "a reset ends it");
|
||||
modes.feed(b"\x1b[?09;\x1b[?1003h");
|
||||
assert_eq!(modes.mode.tracking, Tracking::Any);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+5
-4
@@ -12,6 +12,7 @@ use super::{
|
||||
frame::frame,
|
||||
ink::Ink,
|
||||
input::{self, Copy, Paste},
|
||||
mouse::MouseMode,
|
||||
paint::{self, Look},
|
||||
pointer::Pointer,
|
||||
pty::{Events, Pty, PtyEvent, config, fit},
|
||||
@@ -48,8 +49,8 @@ pub struct TerminalView {
|
||||
/// The cursor's cell in the window, from the last layout.
|
||||
pub(super) caret: Bounds<Pixels>,
|
||||
pub(super) pointer: Pointer,
|
||||
/// X10 mouse reporting, mode 9, as the tap follows it.
|
||||
pub(super) x10: bool,
|
||||
/// The mouse mode, as the tap follows it.
|
||||
pub(super) mouse: MouseMode,
|
||||
/// Keys whose press this terminal saw, so their releases may go out.
|
||||
pub(super) held: HashSet<String>,
|
||||
pub(super) size: (usize, usize),
|
||||
@@ -83,7 +84,7 @@ impl TerminalView {
|
||||
marked: None,
|
||||
caret: Bounds::default(),
|
||||
pointer: Pointer::default(),
|
||||
x10: false,
|
||||
mouse: MouseMode::default(),
|
||||
held: HashSet::new(),
|
||||
size,
|
||||
cell: (8, 16),
|
||||
@@ -155,7 +156,7 @@ impl TerminalView {
|
||||
match event {
|
||||
PtyEvent::Folder(folder) => cx.emit(TerminalEvent::Folder(folder)),
|
||||
PtyEvent::Term(event) => self.term_event(event, cx),
|
||||
PtyEvent::X10(on) => self.x10 = on,
|
||||
PtyEvent::Mouse(mode) => self.mouse = mode,
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
|
||||
Reference in new issue
Block a user