Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for OSC 22 control sequence to change mouse cursor shape #6292

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
8 changes: 8 additions & 0 deletions mux/src/localpane.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,14 @@ pub struct LocalPane {

#[async_trait(?Send)]
impl Pane for LocalPane {
fn get_mouse_cursor_shape(&self) -> Option<String> {
self.terminal.lock().get_mouse_cursor_shape()
}

fn clear_mouse_cursor_shape(&self) {
self.terminal.lock().clear_mosue_cursor_shape();
}

fn pane_id(&self) -> PaneId {
self.pane_id
}
Expand Down
11 changes: 11 additions & 0 deletions mux/src/pane.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,9 @@ pub trait Pane: Downcast + Send + Sync {
/// Returns render related dimensions
fn get_dimensions(&self) -> RenderableDimensions;

fn get_mouse_cursor_shape(&self) -> Option<String>;
fn clear_mouse_cursor_shape(&self);

fn get_title(&self) -> String;
fn send_paste(&self, text: &str) -> anyhow::Result<()>;
fn reader(&self) -> anyhow::Result<Option<Box<dyn std::io::Read + Send>>>;
Expand Down Expand Up @@ -550,6 +553,14 @@ mod test {
}

impl Pane for FakePane {
fn get_mouse_cursor_shape(&self) -> Option<String> {
unimplemented!()
}

fn clear_mouse_cursor_shape(&self) {
unimplemented!();
}

fn pane_id(&self) -> PaneId {
unimplemented!()
}
Expand Down
8 changes: 8 additions & 0 deletions mux/src/tab.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2222,6 +2222,14 @@ mod test {
}

impl Pane for FakePane {
fn get_mouse_cursor_shape(&self) -> Option<String> {
unimplemented!();
}

fn clear_mouse_cursor_shape(&self) {
unimplemented!();
}

fn pane_id(&self) -> PaneId {
self.id
}
Expand Down
6 changes: 6 additions & 0 deletions mux/src/termwiztermtab.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,12 @@ impl TermWizTerminalPane {
}

impl Pane for TermWizTerminalPane {
fn get_mouse_cursor_shape(&self) -> Option<String> {
None
}

fn clear_mouse_cursor_shape(&self) {}

fn pane_id(&self) -> PaneId {
self.pane_id
}
Expand Down
2 changes: 2 additions & 0 deletions term/src/terminal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ pub enum Alert {
TabTitleChanged(Option<String>),
/// When the color palette has been updated
PaletteChanged,
/// When the mouse cursor shape has been updated
MouseCursorShapeChanged,
/// A UserVar has changed value
SetUserVar {
name: String,
Expand Down
20 changes: 20 additions & 0 deletions term/src/terminalstate/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,9 @@ pub struct TerminalState {

palette: Option<ColorPalette>,

/// The mouse cursor shape (OSC 22)
mouse_cursor_shape: Option<String>,

pixel_width: usize,
pixel_height: usize,
dpi: u32,
Expand Down Expand Up @@ -547,6 +550,7 @@ impl TerminalState {
any_event_mouse: false,
button_event_mouse: false,
mouse_tracking: false,
mouse_cursor_shape: None,
last_mouse_move: None,
cursor_visible: true,
g0_charset: CharSet::Ascii,
Expand Down Expand Up @@ -665,6 +669,16 @@ impl TerminalState {
.unwrap_or_else(|| self.config.color_palette())
}

/// Returns a copy of the current mouse cursor shape.
pub fn get_mouse_cursor_shape(&self) -> Option<String> {
self.mouse_cursor_shape.clone()
}

/// Clears the mouse cursor shape.
pub fn clear_mosue_cursor_shape(&mut self) {
self.mouse_cursor_shape = None;
}

/// Called in response to dynamic color scheme escape sequences.
/// Will make a copy of the palette from the config file if this
/// is the first of these escapes we've seen.
Expand Down Expand Up @@ -927,6 +941,12 @@ impl TerminalState {
}
}

fn mouse_cursor_shape_did_change(&mut self) {
if let Some(handler) = self.alert_handler.as_mut() {
handler.alert(Alert::MouseCursorShapeChanged);
}
}

/// When dealing with selection, mark a range of lines as dirty
pub fn make_all_lines_dirty(&mut self) {
let seqno = self.seqno;
Expand Down
6 changes: 6 additions & 0 deletions term/src/terminalstate/performer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,7 @@ impl<'a> Performer<'a> {
self.focus_tracking = false;
self.mouse_tracking = false;
self.mouse_encoding = MouseEncoding::X10;
self.mouse_cursor_shape = None;
self.keyboard_encoding = KeyboardEncoding::Xterm;
self.sixel_scrolls_right = false;
self.any_event_mouse = false;
Expand All @@ -704,6 +705,7 @@ impl<'a> Performer<'a> {
self.erase_in_display(EraseInDisplay::EraseScrollback);
self.erase_in_display(EraseInDisplay::EraseDisplay);
self.palette_did_change();
self.mouse_cursor_shape_did_change();
}

_ => {
Expand All @@ -718,6 +720,10 @@ impl<'a> Performer<'a> {
self.pop_tmux_title_state();
self.flush_print();
match osc {
OperatingSystemCommand::SetMouseCursorShape(mouse_shape) => {
self.mouse_cursor_shape = Some(mouse_shape);
self.mouse_cursor_shape_did_change();
}
OperatingSystemCommand::SetIconNameSun(title)
| OperatingSystemCommand::SetIconName(title) => {
if title.is_empty() {
Expand Down
22 changes: 22 additions & 0 deletions termwiz/src/escape/osc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ pub enum OperatingSystemCommand {
QuerySelection(Selection),
SetSelection(Selection, String),
SystemNotification(String),
SetMouseCursorShape(String),
ITermProprietary(ITermProprietary),
FinalTermSemanticPrompt(FinalTermSemanticPrompt),
ChangeColorNumber(Vec<ChangeColorPair>),
Expand Down Expand Up @@ -312,6 +313,7 @@ impl OperatingSystemCommand {
SetHyperlink => Ok(OperatingSystemCommand::SetHyperlink(Hyperlink::parse(osc)?)),
ManipulateSelectionData => Self::parse_selection(osc),
SystemNotification => single_string!(SystemNotification),
SetMouseCursorShape => single_string!(SetMouseCursorShape),
SetCurrentWorkingDirectory => single_string!(CurrentWorkingDirectory),
ITermProprietary => {
self::ITermProprietary::parse(osc).map(OperatingSystemCommand::ITermProprietary)
Expand Down Expand Up @@ -419,6 +421,7 @@ osc_entries!(
SetHighlightBackgroundColor = "17",
SetTektronixCursorColor = "18",
SetHighlightForegroundColor = "19",
SetMouseCursorShape = "22",
SetLogFileName = "46",
SetFont = "50",
EmacsShell = "51",
Expand Down Expand Up @@ -509,6 +512,7 @@ impl Display for OperatingSystemCommand {
QuerySelection(s) => write!(f, "52;{};?", s)?,
SetSelection(s, val) => write!(f, "52;{};{}", s, base64_encode(val))?,
SystemNotification(s) => write!(f, "9;{}", s)?,
SetMouseCursorShape(s) => write!(f, "22;{}", s)?,
ITermProprietary(i) => i.fmt(f)?,
FinalTermSemanticPrompt(i) => i.fmt(f)?,
ResetColors(colors) => {
Expand Down Expand Up @@ -1362,6 +1366,24 @@ mod test {
);
}

#[test]
fn mouse_cursor_shape() {
assert_eq!(
parse(&["22", "pointer"], "\x1b]22;pointer\x1b\\"),
OperatingSystemCommand::SetMouseCursorShape("pointer".into())
);

assert_eq!(
parse(&["22", "default"], "\x1b]22;default\x1b\\"),
OperatingSystemCommand::SetMouseCursorShape("default".into())
);

assert_eq!(
parse(&["22", ""], "\x1b]22;\x1b\\"),
OperatingSystemCommand::SetMouseCursorShape("".into())
);
}

#[test]
fn hyperlink() {
assert_eq!(
Expand Down
6 changes: 6 additions & 0 deletions wezterm-client/src/pane/clientpane.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,12 @@ impl ClientPane {

#[async_trait(?Send)]
impl Pane for ClientPane {
fn get_mouse_cursor_shape(&self) -> Option<String> {
None
}

fn clear_mouse_cursor_shape(&self) {}

fn pane_id(&self) -> PaneId {
self.local_pane_id
}
Expand Down
1 change: 1 addition & 0 deletions wezterm-gui/src/frontend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ impl GuiFrontEnd {
alert:
Alert::OutputSinceFocusLost
| Alert::PaletteChanged
| Alert::MouseCursorShapeChanged
| Alert::CurrentWorkingDirectoryChanged
| Alert::WindowTitleChanged(_)
| Alert::TabTitleChanged(_)
Expand Down
6 changes: 6 additions & 0 deletions wezterm-gui/src/overlay/copy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1101,6 +1101,12 @@ impl CopyRenderable {
}

impl Pane for CopyOverlay {
fn get_mouse_cursor_shape(&self) -> Option<String> {
None
}

fn clear_mouse_cursor_shape(&self) {}

fn pane_id(&self) -> PaneId {
self.delegate.pane_id()
}
Expand Down
6 changes: 6 additions & 0 deletions wezterm-gui/src/overlay/quickselect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,12 @@ impl QuickSelectOverlay {
}

impl Pane for QuickSelectOverlay {
fn get_mouse_cursor_shape(&self) -> Option<String> {
None
}

fn clear_mouse_cursor_shape(&self) {}

fn pane_id(&self) -> PaneId {
self.delegate.pane_id()
}
Expand Down
22 changes: 22 additions & 0 deletions wezterm-gui/src/termwindow/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1209,6 +1209,12 @@ impl TermWindow {
} => {
self.update_title();
}
MuxNotification::Alert {
alert: Alert::MouseCursorShapeChanged,
pane_id,
} => {
self.mux_pane_output_event(pane_id);
}
MuxNotification::Alert {
alert: Alert::PaletteChanged,
pane_id,
Expand Down Expand Up @@ -1519,6 +1525,12 @@ impl TermWindow {
} => {
// fall through
}
MuxNotification::Alert {
alert: Alert::MouseCursorShapeChanged { .. },
..
} => {
// fall through
}
Comment on lines +1528 to +1533
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I copied what's done for Alert::PaletteChanged here, not sure if that's correct.

}

window.notify(TermWindowNotif::MuxNotification(n));
Expand Down Expand Up @@ -3528,6 +3540,16 @@ impl TermWindow {
}
}

fn parse_mouse_cursor_shape(mouse_cursor_shape: &str) -> MouseCursor {
match mouse_cursor_shape {
"pointer" => MouseCursor::Hand,
"text" => MouseCursor::Text,
"row-resize" | "ns-resize" => MouseCursor::SizeUpDown,
"col-resize" | "ew-resize" => MouseCursor::SizeLeftRight,
_ => MouseCursor::Arrow,
}
}

impl Drop for TermWindow {
fn drop(&mut self) {
self.clear_all_overlays();
Expand Down
15 changes: 13 additions & 2 deletions wezterm-gui/src/termwindow/mouseevent.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::tabbar::TabBarItem;
use crate::termwindow::{
GuiWin, MouseCapture, PositionedSplit, ScrollHit, TermWindowNotif, UIItem, UIItemType, TMB,
parse_mouse_cursor_shape, GuiWin, MouseCapture, PositionedSplit, ScrollHit, TermWindowNotif,
UIItem, UIItemType, TMB,
};
use ::window::{
MouseButtons as WMB, MouseCursor, MouseEvent, MouseEventKind as WMEK, MousePress,
Expand Down Expand Up @@ -837,7 +838,17 @@ impl super::TermWindow {
// When hovering over a hyperlink, show an appropriate
// mouse cursor to give the cue that it is clickable
MouseCursor::Hand
} else if pane.is_mouse_grabbed() || outside_window {
} else if outside_window {
MouseCursor::Arrow
} else if let Some(shape) = pane.get_mouse_cursor_shape() {
if pane.is_mouse_grabbed() {
parse_mouse_cursor_shape(shape.as_str())
} else {
// If the mouse is no longer grabbed by a TUI, reset the cursor shape.
pane.clear_mouse_cursor_shape();
MouseCursor::Arrow
}
} else if pane.is_mouse_grabbed() {
Comment on lines +843 to +851
Copy link
Author

@reykjalin reykjalin Oct 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As it turns out, this fixed the issue of the mouse cursor shape being wrong. I have absolutely no idea if this is the right way to approach it though.

But basically; if the mouse is grabbed by whichever application is running, we defer to whatever mouse shape is set by any OSC 22 escape sequences.

If we find that a shape has been set by an OSC 22 escape sequence but the mouse isn't grabbed we clear it and just use the default pointer shape instead.

Some thoughts:

  1. I'm realizing that using mouse_cursor_shape for the names of this might be confusing? Should we specify that this is a shape set through an OSC 22 escape sequence somehow?
    • It's more confusing in the GUI layer, less so in the terminal state itself.
    • But it may be useful to use something like osc_22_mouse_cursor_shape instead of just mouse_cursor_shape?
  2. I'm not sure this is the best place to clear the mouse cursor shape. Maybe there's an event sent when the mouse is grabbed and released that would be more appropriate?
  3. As stated earlier; I have no clue if this is the right way to manage OSC 22 control sequences, but at least it behaves the way I expect.
Before this change After this change
Screen.Recording.2024-10-22.at.2.36.17.PM.mov
Screen.Recording.2024-10-22.at.2.31.45.PM.mov

Happy to get some guidance on any improvements here!

MouseCursor::Arrow
} else {
MouseCursor::Text
Expand Down