mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-06-15 08:03:56 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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" {
|
||||
|
||||
@@ -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" {
|
||||
|
||||
Reference in New Issue
Block a user