diff --git a/src/Surface.zig b/src/Surface.zig index f6f5e7b99..c56c9791c 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..0a5fa0249 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`. @@ -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" { diff --git a/src/terminal/osc/parsers/semantic_prompt.zig b/src/terminal/osc/parsers/semantic_prompt.zig index 25152d7c3..97212fbb3 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, @@ -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" {