From 3263ce5122480fe4d59a1189f67a3aac65949aa1 Mon Sep 17 00:00:00 2001 From: Alex Bennett Date: Sun, 7 Jun 2026 23:01:53 +0800 Subject: [PATCH 1/3] terminal: support click_events=2 --- src/Surface.zig | 12 ++++++++---- src/inspector/widgets/screen.zig | 2 +- src/terminal/Screen.zig | 2 +- src/terminal/Terminal.zig | 6 ++---- src/terminal/osc/parsers/semantic_prompt.zig | 19 +++++++++++++++++-- 5 files changed, 29 insertions(+), 12 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index f6f5e7b99..2ab09d371 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -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 diff --git a/src/inspector/widgets/screen.zig b/src/inspector/widgets/screen.zig index bfe961f45..41c113e75 100644 --- a/src/inspector/widgets/screen.zig +++ b/src/inspector/widgets/screen.zig @@ -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), } } diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 8ee700252..5c39be02f 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -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, }; }; diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 3a914794f..b96084df6 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -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`. diff --git a/src/terminal/osc/parsers/semantic_prompt.zig b/src/terminal/osc/parsers/semantic_prompt.zig index 25152d7c3..2648d2859 100644 --- a/src/terminal/osc/parsers/semantic_prompt.zig +++ b/src/terminal/osc/parsers/semantic_prompt.zig @@ -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, From 97777cf5a32fd8943ab5c15d6ccee709a15943b4 Mon Sep 17 00:00:00 2001 From: Alex Bennett Date: Sun, 7 Jun 2026 23:55:47 +0800 Subject: [PATCH 2/3] fix: fix tests, change enum fields to lowercase --- src/Surface.zig | 4 ++-- src/terminal/Terminal.zig | 23 +++++++++++++++++--- src/terminal/osc/parsers/semantic_prompt.zig | 11 +++++----- 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 2ab09d371..2ce7af852 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -4222,8 +4222,8 @@ fn maybePromptClick(self: *Surface) !bool { // 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 }, + .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( diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index b96084df6..0a5fa0249 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -1226,7 +1226,7 @@ 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| { - screen.semantic_prompt.click = .{ .click_events = v } ; + screen.semantic_prompt.click = .{ .click_events = v }; break :click; } @@ -12419,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" { @@ -12476,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" { diff --git a/src/terminal/osc/parsers/semantic_prompt.zig b/src/terminal/osc/parsers/semantic_prompt.zig index 2648d2859..97212fbb3 100644 --- a/src/terminal/osc/parsers/semantic_prompt.zig +++ b/src/terminal/osc/parsers/semantic_prompt.zig @@ -68,7 +68,7 @@ pub const Command = struct { // 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 ClickEvents = enum { absolute, relative }; pub const Option = enum { aid, @@ -211,8 +211,8 @@ pub const Option = enum { else null, .click_events => if (value.len == 1) switch (value[0]) { - '1' => .Absolute, - '2' => .Relative, + '1' => .absolute, + '2' => .relative, else => null, } else null, .special_key => if (value.len == 1) switch (value[0]) { @@ -1264,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" { From fd882c67cc3d95e9cc7fd837f33fbd0ef64017c4 Mon Sep 17 00:00:00 2001 From: Alex Bennett Date: Mon, 8 Jun 2026 16:50:41 +0800 Subject: [PATCH 3/3] pr review: added saturated arithmetic --- src/Surface.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 2ce7af852..c56c9791c 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -4222,8 +4222,8 @@ fn maybePromptClick(self: *Surface) !bool { // 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 }, + .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(