Handle Kitty click_events OSC133 extension

This commit is contained in:
Mitchell Hashimoto
2026-02-02 09:47:35 -08:00
parent b827e587d9
commit 25b6423e7b

View File

@@ -3969,6 +3969,15 @@ pub fn mouseButtonCallback(
log.warn("error processing links err={}", .{err});
}
}
// Handle prompt clicking. If we released our mouse on a prompt
// and we support some kind of click events, then we need to
// move to it.
if (self.maybePromptClick()) |handled| {
if (handled) return true;
} else |err| {
log.warn("error processing prompt click err={}", .{err});
}
}
// Report mouse events if enabled
@@ -4188,10 +4197,6 @@ pub fn mouseButtonCallback(
.y = pt_viewport.y,
},
}) orelse {
// Weird... our viewport x/y that we just converted isn't
// found in our pages. This is probably a bug but we don't
// want to crash in releases because its harmless. So, we
// only assert in debug mode.
if (comptime std.debug.runtime_safety) unreachable;
break :sel;
};
@@ -4278,6 +4283,106 @@ pub fn mouseButtonCallback(
return false;
}
fn maybePromptClick(self: *Surface) !bool {
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
const t: *terminal.Terminal = self.renderer_state.terminal;
const screen: *terminal.Screen = t.screens.active;
// If our screen doesn't handle any prompt clicks, then we never
// do anything.
if (screen.semantic_prompt.click == .none) return false;
// If our cursor isn't currently at a prompt then we don't handle
// prompt clicks because we can't move if we're not in a prompt!
if (!t.cursorIsAtPrompt()) return false;
// If we have a selection currently, then releasing the mouse
// completes the selection and we don't do prompt moving. I don't
// love this logic, I think it should be generalized to "if the
// mouse release was on a different cell than the mouse press" but
// our mouse state at the time of writing this doesn't support that.
if (screen.selection != null) return false;
// Get the pin for our mouse click.
const pos = try self.rt_surface.getCursorPos();
const pos_vp = self.posToViewport(pos.x, pos.y);
const click_pin: terminal.Pin = pin: {
const pin = screen.pages.pin(.{
.viewport = .{
.x = pos_vp.x,
.y = pos_vp.y,
},
}) orelse {
// See mouseButtonCallback for explanation
if (comptime std.debug.runtime_safety) unreachable;
return false;
};
break :pin pin;
};
// Get our cursor's most current prompt.
const prompt_pin: terminal.Pin = prompt_pin: {
var it = screen.cursor.page_pin.promptIterator(
.left_up,
null,
);
break :prompt_pin it.next() orelse {
// This shouldn't be possible because we asserted we're at
// a prompt above, so we MUST find some prompt in a left_up search.
log.warn("cursor is at prompt but no prompt found", .{});
if (comptime std.debug.runtime_safety) unreachable;
return false;
};
};
// If our mouse click is before the prompt, we don't move.
// We DO ALLOW clicks AFTER the prompt, specifically with Kitty's
// click_events=1 since we rely on the shell to validate out of
// bounds clicks. This matches Kitty's logic as best I can tell.
if (click_pin.before(prompt_pin)) return false;
// At this point we've established:
// - Screen supports prompt clicks
// - Cursor is at a prompt
// - Click is at or below our prompt
switch (screen.semantic_prompt.click) {
// Guarded at the start of this function
.none => unreachable,
.click_events => {
// For the event, we always send a left-click press event.
// This matches what Kitty sends.
var data: termio.Message.WriteReq.Small.Array = undefined;
const resp = try std.fmt.bufPrint(
&data,
"\x1B[<0;{d};{d}M",
.{ pos_vp.x + 1, pos_vp.y + 1 },
);
// Not that noisy since this only happens on prompt clicks.
log.debug(
"sending click_events=1 event=ESC{s}",
.{resp[1..]},
);
// Ask our IO thread to write the data
self.queueIo(.{ .write_small = .{
.data = data,
.len = @intCast(resp.len),
} }, .locked);
},
.cl => |cl| {
// TODO: Handle these
_ = cl;
},
}
return true;
}
/// Performs the "click-to-move" logic to move the cursor to the given
/// screen point if possible. This works by converting the path to the
/// given point into a series of arrow key inputs.