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

Conversation

reykjalin
Copy link

@reykjalin reykjalin commented Oct 18, 2024

Description

  • Adds handling for the OSC 22 control sequence.
  • Follows the example set by Kitty to support CSS cursor values for the OSC 22 parameter.
  • Currently only supports pointer, text, row-resize, ns-resize, col-resize, and ew-resize, based on the currently available MouseCursor options in the codebase. Any other value defaults to MouseCursor::Arrow.

Videos

Running comlink (terminal IRC client) before and after:

Before (9ddca7b) After
Screen.Recording.2024-10-17.at.11.39.06.PM.mov
Screen.Recording.2024-10-22.at.2.31.45.PM.mov

Testing

This requires a terminal program that makes use of OSC 22 codes to change the mouse cursor. The only one I know of and actively use is comlink, but I'm sure there are other TUIs out there that do use the codes, I'm just not aware of them.

Run a terminal program that uses OSC 22 codes and check if they work.

I'll try to make a terminal program to demonstrate at least some of them as I work on this PR. It should make testing easier.

@reykjalin reykjalin changed the title Add support for OSC 22 control sequence to change mouse shape Add support for OSC 22 control sequence to change mouse cursor shape Oct 18, 2024
term/src/terminalstate/mod.rs Outdated Show resolved Hide resolved
term/src/terminalstate/mod.rs Outdated Show resolved Hide resolved
wezterm-client/src/pane/clientpane.rs Outdated Show resolved Hide resolved
wezterm-gui/src/overlay/copy.rs Outdated Show resolved Hide resolved
wezterm-gui/src/overlay/quickselect.rs Outdated Show resolved Hide resolved
wezterm-gui/src/termwindow/mod.rs Show resolved Hide resolved
Copy link
Member

@wez wez left a comment

Choose a reason for hiding this comment

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

I'm surprised that you got to this so soon!
A few suggestions below!

mux/src/pane.rs Outdated Show resolved Hide resolved
term/src/terminalstate/mod.rs Outdated Show resolved Hide resolved
term/src/terminalstate/mod.rs Outdated Show resolved Hide resolved
term/src/terminalstate/mod.rs Outdated Show resolved Hide resolved
term/src/terminalstate/mod.rs Outdated Show resolved Hide resolved
term/src/terminalstate/performer.rs Outdated Show resolved Hide resolved
wezterm-gui/src/termwindow/mod.rs Outdated Show resolved Hide resolved
wezterm-gui/src/termwindow/mouseevent.rs Outdated Show resolved Hide resolved
window/Cargo.toml Outdated Show resolved Hide resolved
@reykjalin
Copy link
Author

I'm surprised that you got to this so soon!

You and me both 😅

I think I've addressed most of the feedback, but I'm still seeing the issue where it's like the mouse shape is cached. I'll dig into that some more next week :)

wezterm-gui/src/termwindow/mod.rs Outdated Show resolved Hide resolved
wezterm-gui/src/termwindow/mouseevent.rs Outdated Show resolved Hide resolved
@reykjalin
Copy link
Author

reykjalin commented Oct 21, 2024

I think I figured out the reason the mouse cursor shape gets "stuck" on whatever was last set through the OSC 22 sequence. It's because the TUI doesn't set the OSC22 code back to the default, or doesn't clear it I guess? So once the TUI is closed whichever pointer was active is still stored in mouse_cursor_shape in the termstate, so it keeps getting applied over everything else.

I'm not sure if that's "correct" behavior or not. I also don't know what the right time to clear the OSC 22 setting would be, if that even is the correct approach here. The xterm docs don't seem to specify that, at least not in relation to what the control sequence does.

I'll keep looking into this over the next couple of days.

Comment on lines +843 to +851
} 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() {
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!

Copy link
Member

Choose a reason for hiding this comment

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

I think we should implicitly clear the cursor shape when the mouse is un-grabbed, then the original gating logic:

else if pane.is_mouse_grabbed() || outside_window {
   MouseCursor::Arrow

can be used there to very clearly show that we're not going to use the cursor shape in those conditions.

Each of these locations are places where the effective result is is_mouse_grabbed might change to false:

Mode::ResetDecPrivateMode(DecPrivateMode::Code(DecPrivateModeCode::MouseTracking)) => {
self.mouse_tracking = false;
self.last_mouse_move.take();
}

Mode::ResetDecPrivateMode(DecPrivateMode::Code(DecPrivateModeCode::AnyEventMouse)) => {
self.any_event_mouse = false;
self.last_mouse_move.take();
}

Mode::ResetDecPrivateMode(DecPrivateMode::Code(
DecPrivateModeCode::ButtonEventMouse,
)) => {
self.button_event_mouse = false;
self.last_mouse_move.take();
}

There should probably be a little helper function called in those three cases that does something like:

if !self.is_mouse_grabbed() && self.mouse_cursor_shape.is_some() {
   self.mouse_cursor_shape.take();
   mouse_cursor_shape_did_change
}

with all that done, the logic here in this file should simplify a lot and be very clear and easy to follow.

FWIW, I think mouse_cursor_shape is OK to refer to this stuff.

@reykjalin reykjalin marked this pull request as ready for review October 22, 2024 18:39
@reykjalin reykjalin requested a review from wez November 1, 2024 03:04
@reykjalin
Copy link
Author

Hi @wez 👋 Just wanted to ask if there's anything I can do to make moving this along easier?

I think the changes here are at least functionally good, so I'd love to help move this along if there's anything I can do to help with that :)

Copy link
Member

@wez wez left a comment

Choose a reason for hiding this comment

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

Thanks for this!
A few comments here.

You should also add OSC 22 to https://wezterm.org/escape-sequences.html so that this support is discoverable.

@@ -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);
Copy link
Member

Choose a reason for hiding this comment

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

IMO, clear_mouse_cursor_shape doesn't belong in this interface.
The terminal itself owns the current shape, and the Pane should just report its state.
I don't think it makes sense for other layers to poke it, which is good because that is one less RPC call that will need to be added to the mux protocol to accommodate this change.

@@ -550,6 +553,14 @@ mod test {
}

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

Choose a reason for hiding this comment

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

Suggested change
unimplemented!()
None

@@ -2222,6 +2222,14 @@ mod test {
}

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

Choose a reason for hiding this comment

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

Suggested change
unimplemented!();
None

@@ -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,
Copy link
Member

Choose a reason for hiding this comment

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

Let's pass the name along with this, like with the title changed event above.
That will make it easier to do a half-baked mux client implementation (see 44866cc)

Suggested change
MouseCursorShapeChanged,
MouseCursorShapeChanged(Option<String>)

Copy link
Member

Choose a reason for hiding this comment

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

Note that by changing the Alert enum, you're changing the binary signature of types on which the mux protocol depends. You'll need to bump the CODEC_VERSION:

wezterm/codec/src/lib.rs

Lines 441 to 444 in c06cc87

/// The overall version of the codec.
/// This must be bumped when backwards incompatible changes
/// are made to the types and protocol.
pub const CODEC_VERSION: usize = 44;

Comment on lines +677 to +681
/// Clears the mouse cursor shape.
pub fn clear_mosue_cursor_shape(&mut self) {
self.mouse_cursor_shape = None;
}

Copy link
Member

Choose a reason for hiding this comment

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

as mentioned elsewhere, I think this should be removed from the interface

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

Comment on lines +843 to +851
} 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() {
Copy link
Member

Choose a reason for hiding this comment

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

I think we should implicitly clear the cursor shape when the mouse is un-grabbed, then the original gating logic:

else if pane.is_mouse_grabbed() || outside_window {
   MouseCursor::Arrow

can be used there to very clearly show that we're not going to use the cursor shape in those conditions.

Each of these locations are places where the effective result is is_mouse_grabbed might change to false:

Mode::ResetDecPrivateMode(DecPrivateMode::Code(DecPrivateModeCode::MouseTracking)) => {
self.mouse_tracking = false;
self.last_mouse_move.take();
}

Mode::ResetDecPrivateMode(DecPrivateMode::Code(DecPrivateModeCode::AnyEventMouse)) => {
self.any_event_mouse = false;
self.last_mouse_move.take();
}

Mode::ResetDecPrivateMode(DecPrivateMode::Code(
DecPrivateModeCode::ButtonEventMouse,
)) => {
self.button_event_mouse = false;
self.last_mouse_move.take();
}

There should probably be a little helper function called in those three cases that does something like:

if !self.is_mouse_grabbed() && self.mouse_cursor_shape.is_some() {
   self.mouse_cursor_shape.take();
   mouse_cursor_shape_did_change
}

with all that done, the logic here in this file should simplify a lot and be very clear and easy to follow.

FWIW, I think mouse_cursor_shape is OK to refer to this stuff.

@@ -245,6 +245,12 @@ impl ClientPane {

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

Choose a reason for hiding this comment

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

Take a look at the changes over in 44866cc#diff-b8703182c50d1b02b59221d8680fa9e6ec9eeb8520beb8e34d85f933fca7fac5R195 for how to do an imperfect but probably good enough in practice implementation of this that will work over the mux protocol

@reykjalin
Copy link
Author

Thanks for the review Wez! Just want to let you know this is on my radar, but I'm going to be busy until the end of March so I might not be able to address the review until then.

I'll follow up as soon as I'm able to! :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants