terminal: support click_events=2 (#12953)

## Context 

Implements the OSC 133 `click_events=2` such that click events on the
terminal are sent to the shell where the y coordinate is sent relative
to the prompt pin.

See
https://sw.kovidgoyal.net/kitty/shell-integration/#click_events=1|2:~:text=click%5Fevents,xterm
further detailed explanation.

This should close https://github.com/ghostty-org/ghostty/issues/10865

### Testing

I did some basic manual testing here via the following: 
```
ghostty  main
❯ printf '\033]133;A;click_events=1\007' && cat
^[[<0;49;4M^[[<0;48;8M^[[<0;47;12M^C

ghostty  main  5s
❯ printf '\033]133;A;click_events=2\007' && cat
^[[<0;50;1M^[[<0;49;4M^[[<0;48;9M^[[<0;48;14M^C
```

#### Notes  	  

This is my first Ghostty PR. All code here is hand-rolled, AI was used
to do "smart" code searches/point me in the right direction.

I'm actively trying to learn Zig, Ghostty, and get more involved with
the community here so I'm avoiding obtuse usage of AI where possible.

Any feedback or tips are more than welcome! Thank you! 🙏
This commit is contained in:
Mitchell Hashimoto
2026-06-08 06:31:21 -07:00
committed by GitHub
5 changed files with 51 additions and 16 deletions

View File

@@ -4218,20 +4218,24 @@ fn maybePromptClick(self: *Surface) !bool {
// Guarded at the start of this function
.none => unreachable,
.click_events => {
.click_events => |v| {
// For the event, we always send a left-click press event.
// This matches what Kitty sends.
const key: u8, const y: u32 = switch (v) {
.absolute => .{ 1, pos_vp.y +| 1 },
.relative => .{ 2, pos_vp.y -| prompt_pin.y +| 1 },
};
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 },
.{ pos_vp.x + 1, y },
);
// Not that noisy since this only happens on prompt clicks.
log.debug(
"sending click_events=1 event=ESC{s}",
.{resp[1..]},
"sending click_events={} event=ESC{s}",
.{ key, resp[1..] },
);
// Ask our IO thread to write the data

View File

@@ -393,7 +393,7 @@ pub fn semanticPromptTable(
_ = cimgui.c.ImGui_TableSetColumnIndex(1);
switch (semantic_prompt.click) {
.none => cimgui.c.ImGui_TextDisabled("(none)"),
.click_events => cimgui.c.ImGui_Text("click_events"),
.click_events => |click_events| cimgui.c.ImGui_Text("click_events=%s", @tagName(click_events).ptr),
.cl => |cl| cimgui.c.ImGui_Text("cl=%s", @tagName(cl).ptr),
}
}

View File

@@ -113,7 +113,7 @@ pub const SemanticPrompt = struct {
pub const SemanticClick = union(enum) {
none,
click_events,
click_events: osc.semantic_prompt.ClickEvents,
cl: osc.semantic_prompt.Click,
};
};

View File

@@ -1226,10 +1226,8 @@ pub fn semanticPrompt(
// within a prompt area to SGR mouse events and defers to the
// shell to handle them.
if (cmd.readOption(.click_events)) |v| {
if (v) {
screen.semantic_prompt.click = .click_events;
break :click;
}
screen.semantic_prompt.click = .{ .click_events = v };
break :click;
}
// If click_events was not set or disabled, fallback to `cl`.
@@ -12421,7 +12419,24 @@ test "Terminal: OSC133A click_events=1 sets click to click_events" {
.options_unvalidated = "click_events=1",
});
try testing.expectEqual(.click_events, t.screens.active.semantic_prompt.click);
try testing.expectEqual(Screen.SemanticPrompt.SemanticClick{ .click_events = .absolute }, t.screens.active.semantic_prompt.click);
}
test "Terminal: OSC133A click_events=2 sets click to click_events (relative)" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
// Verify default state is none
try testing.expectEqual(.none, t.screens.active.semantic_prompt.click);
// OSC 133;A with click_events=2
try t.semanticPrompt(.{
.action = .fresh_line_new_prompt,
.options_unvalidated = "click_events=2",
});
try testing.expectEqual(Screen.SemanticPrompt.SemanticClick{ .click_events = .relative }, t.screens.active.semantic_prompt.click);
}
test "Terminal: OSC133A click_events=0 does not set click_events" {
@@ -12478,7 +12493,7 @@ test "Terminal: OSC133A click_events=1 takes priority over cl" {
});
// click_events should take priority
try testing.expectEqual(.click_events, t.screens.active.semantic_prompt.click);
try testing.expectEqual(Screen.SemanticPrompt.SemanticClick{ .click_events = .absolute }, t.screens.active.semantic_prompt.click);
}
test "Terminal: OSC133A click_events=0 falls back to cl" {

View File

@@ -60,6 +60,16 @@ pub const Command = struct {
}
};
// ClickEvents can either be a click_events=1 or click_events=2.
// The click_events=1 sends a click event with the absolute coordinates
// of the click.
// The click_events=2 sends a click event with the coordinates of the click
// relative to the prompt area.
// See https://github.com/ghostty-org/ghostty/issues/10865 and
// https://github.com/kovidgoyal/kitty/issues/9500
// for further details.
pub const ClickEvents = enum { absolute, relative };
pub const Option = enum {
aid,
cl,
@@ -102,7 +112,7 @@ pub const Option = enum {
.err => []const u8,
.redraw => Redraw,
.special_key => bool,
.click_events => bool,
.click_events => ClickEvents,
.cmdline => []const u8,
.cmdline_url => []const u8,
.exit_code => i32,
@@ -200,7 +210,12 @@ pub const Option = enum {
.last
else
null,
.special_key, .click_events => if (value.len == 1) switch (value[0]) {
.click_events => if (value.len == 1) switch (value[0]) {
'1' => .absolute,
'2' => .relative,
else => null,
} else null,
.special_key => if (value.len == 1) switch (value[0]) {
'0' => false,
'1' => true,
else => null,
@@ -1249,9 +1264,10 @@ test "Option.read special_key" {
test "Option.read click_events" {
const testing = std.testing;
try testing.expect(Option.click_events.read("click_events=1").? == true);
try testing.expect(Option.click_events.read("click_events=0").? == false);
try testing.expect(Option.click_events.read("click_events=yes") == null);
try testing.expect(Option.click_events.read("click_events=0") == null);
try testing.expect(Option.click_events.read("click_events=1").? == .absolute);
try testing.expect(Option.click_events.read("click_events=2").? == .relative);
}
test "Option.read exit_code" {