mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 16:56:09 +03:00
Merge pull request #1387 from mattrobenolt/cmd-click
Open links with Super+click
This commit is contained in:
@ -198,6 +198,7 @@ const DerivedConfig = struct {
|
|||||||
const Link = struct {
|
const Link = struct {
|
||||||
regex: oni.Regex,
|
regex: oni.Regex,
|
||||||
action: input.Link.Action,
|
action: input.Link.Action,
|
||||||
|
highlight: input.Link.Highlight,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn init(alloc_gpa: Allocator, config: *const configpkg.Config) !DerivedConfig {
|
pub fn init(alloc_gpa: Allocator, config: *const configpkg.Config) !DerivedConfig {
|
||||||
@ -215,6 +216,7 @@ const DerivedConfig = struct {
|
|||||||
try links.append(.{
|
try links.append(.{
|
||||||
.regex = regex,
|
.regex = regex,
|
||||||
.action = link.action,
|
.action = link.action,
|
||||||
|
.highlight = link.highlight,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -814,6 +816,35 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Call this when modifiers change. This is safe to call even if modifiers
|
||||||
|
/// match the previous state.
|
||||||
|
///
|
||||||
|
/// This is not publicly exported because modifier changes happen implicitly
|
||||||
|
/// on mouse callbacks, key callbacks, etc.
|
||||||
|
///
|
||||||
|
/// The renderer state mutex MUST NOT be held.
|
||||||
|
fn modsChanged(self: *Surface, mods: input.Mods) void {
|
||||||
|
// The only place we keep track of mods currently is on the mouse.
|
||||||
|
if (!self.mouse.mods.equal(mods)) {
|
||||||
|
// The mouse mods only contain binding modifiers since we don't
|
||||||
|
// want caps/num lock or sided modifiers to affect the mouse.
|
||||||
|
self.mouse.mods = mods.binding();
|
||||||
|
|
||||||
|
// We also need to update the renderer so it knows if it
|
||||||
|
// should highlight links.
|
||||||
|
{
|
||||||
|
self.renderer_state.mutex.lock();
|
||||||
|
defer self.renderer_state.mutex.unlock();
|
||||||
|
self.renderer_state.mouse.mods = mods;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.queueRender() catch |err| {
|
||||||
|
// Not a big deal if this fails.
|
||||||
|
log.warn("failed to notify renderer of mods change err={}", .{err});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Called when our renderer health state changes.
|
/// Called when our renderer health state changes.
|
||||||
fn updateRendererHealth(self: *Surface, health: renderer.Health) void {
|
fn updateRendererHealth(self: *Surface, health: renderer.Health) void {
|
||||||
log.warn("renderer health status change status={}", .{health});
|
log.warn("renderer health status change status={}", .{health});
|
||||||
@ -1352,10 +1383,12 @@ pub fn keyCallback(
|
|||||||
// to hide it again if it was hidden.
|
// to hide it again if it was hidden.
|
||||||
const rehide = self.mouse.hidden;
|
const rehide = self.mouse.hidden;
|
||||||
|
|
||||||
|
// Update our modifiers, this will update mouse mods too
|
||||||
|
self.modsChanged(event.mods);
|
||||||
|
|
||||||
// We set this to null to force link reprocessing since
|
// We set this to null to force link reprocessing since
|
||||||
// mod changes can affect link highlighting.
|
// mod changes can affect link highlighting.
|
||||||
self.mouse.link_point = null;
|
self.mouse.link_point = null;
|
||||||
self.mouse.mods = event.mods;
|
|
||||||
const pos = self.rt_surface.getCursorPos() catch break :mouse_mods;
|
const pos = self.rt_surface.getCursorPos() catch break :mouse_mods;
|
||||||
self.cursorPosCallback(pos) catch {};
|
self.cursorPosCallback(pos) catch {};
|
||||||
if (rehide) self.hideMouse();
|
if (rehide) self.hideMouse();
|
||||||
@ -1969,11 +2002,13 @@ pub fn mouseButtonCallback(
|
|||||||
|
|
||||||
// Always record our latest mouse state
|
// Always record our latest mouse state
|
||||||
self.mouse.click_state[@intCast(@intFromEnum(button))] = action;
|
self.mouse.click_state[@intCast(@intFromEnum(button))] = action;
|
||||||
self.mouse.mods = @bitCast(mods);
|
|
||||||
|
|
||||||
// Always show the mouse again if it is hidden
|
// Always show the mouse again if it is hidden
|
||||||
if (self.mouse.hidden) self.showMouse();
|
if (self.mouse.hidden) self.showMouse();
|
||||||
|
|
||||||
|
// Update our modifiers if they changed
|
||||||
|
self.modsChanged(mods);
|
||||||
|
|
||||||
// This is set to true if the terminal is allowed to capture the shift
|
// This is set to true if the terminal is allowed to capture the shift
|
||||||
// modifer. Note we can do this more efficiently probably with less
|
// modifer. Note we can do this more efficiently probably with less
|
||||||
// locking/unlocking but clicking isn't that frequent enough to be a
|
// locking/unlocking but clicking isn't that frequent enough to be a
|
||||||
@ -2252,6 +2287,11 @@ fn linkAtPos(
|
|||||||
|
|
||||||
// Go through each link and see if we clicked it
|
// Go through each link and see if we clicked it
|
||||||
for (self.config.links) |link| {
|
for (self.config.links) |link| {
|
||||||
|
switch (link.highlight) {
|
||||||
|
.always, .hover => {},
|
||||||
|
.always_mods, .hover_mods => |v| if (!v.equal(self.mouse.mods)) continue,
|
||||||
|
}
|
||||||
|
|
||||||
var it = strmap.searchIterator(link.regex);
|
var it = strmap.searchIterator(link.regex);
|
||||||
while (true) {
|
while (true) {
|
||||||
var match = (try it.next()) orelse break;
|
var match = (try it.next()) orelse break;
|
||||||
|
@ -432,8 +432,9 @@ command: ?[]const u8 = null,
|
|||||||
/// TODO: This can't currently be set!
|
/// TODO: This can't currently be set!
|
||||||
link: RepeatableLink = .{},
|
link: RepeatableLink = .{},
|
||||||
|
|
||||||
/// Enable URL matching. URLs are matched on hover and open using the default
|
/// Enable URL matching. URLs are matched on hover with control (Linux) or
|
||||||
/// system application for the linked URL.
|
/// super (macOS) pressed and open using the default system application for
|
||||||
|
/// the linked URL.
|
||||||
///
|
///
|
||||||
/// The URL matcher is always lowest priority of any configured links (see
|
/// The URL matcher is always lowest priority of any configured links (see
|
||||||
/// `link`). If you want to customize URL matching, use `link` and disable this.
|
/// `link`). If you want to customize URL matching, use `link` and disable this.
|
||||||
@ -1437,7 +1438,7 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config {
|
|||||||
try result.link.links.append(alloc, .{
|
try result.link.links.append(alloc, .{
|
||||||
.regex = url.regex,
|
.regex = url.regex,
|
||||||
.action = .{ .open = {} },
|
.action = .{ .open = {} },
|
||||||
.highlight = .{ .hover = {} },
|
.highlight = .{ .hover_mods = inputpkg.ctrlOrSuper(.{}) },
|
||||||
});
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
const Link = @This();
|
const Link = @This();
|
||||||
|
|
||||||
const oni = @import("oniguruma");
|
const oni = @import("oniguruma");
|
||||||
|
const Mods = @import("key.zig").Mods;
|
||||||
|
|
||||||
/// The regular expression that will be used to match the link. Ownership
|
/// The regular expression that will be used to match the link. Ownership
|
||||||
/// of this memory is up to the caller. The link will never free this memory.
|
/// of this memory is up to the caller. The link will never free this memory.
|
||||||
@ -30,6 +31,13 @@ pub const Highlight = union(enum) {
|
|||||||
|
|
||||||
/// Only highlight the link when the mouse is hovering over it.
|
/// Only highlight the link when the mouse is hovering over it.
|
||||||
hover: void,
|
hover: void,
|
||||||
|
|
||||||
|
/// Highlight anytime the given mods are pressed, either when
|
||||||
|
/// hovering or always. For always, all links will be highlighted
|
||||||
|
/// when the mods are pressed regardless of if the mouse is hovering
|
||||||
|
/// over them.
|
||||||
|
always_mods: Mods,
|
||||||
|
hover_mods: Mods,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Returns a new oni.Regex that can be used to match the link.
|
/// Returns a new oni.Regex that can be used to match the link.
|
||||||
|
@ -1555,6 +1555,7 @@ fn rebuildCells(
|
|||||||
arena_alloc,
|
arena_alloc,
|
||||||
screen,
|
screen,
|
||||||
mouse.point orelse .{},
|
mouse.point orelse .{},
|
||||||
|
mouse.mods,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Determine our x/y range for preedit. We don't want to render anything
|
// Determine our x/y range for preedit. We don't want to render anything
|
||||||
|
@ -991,6 +991,7 @@ pub fn rebuildCells(
|
|||||||
arena_alloc,
|
arena_alloc,
|
||||||
screen,
|
screen,
|
||||||
mouse.point orelse .{},
|
mouse.point orelse .{},
|
||||||
|
mouse.mods,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Determine our x/y range for preedit. We don't want to render anything
|
// Determine our x/y range for preedit. We don't want to render anything
|
||||||
|
@ -4,6 +4,7 @@ const std = @import("std");
|
|||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const Inspector = @import("../inspector/main.zig").Inspector;
|
const Inspector = @import("../inspector/main.zig").Inspector;
|
||||||
const terminal = @import("../terminal/main.zig");
|
const terminal = @import("../terminal/main.zig");
|
||||||
|
const inputpkg = @import("../input.zig");
|
||||||
const renderer = @import("../renderer.zig");
|
const renderer = @import("../renderer.zig");
|
||||||
|
|
||||||
/// The mutex that must be held while reading any of the data in the
|
/// The mutex that must be held while reading any of the data in the
|
||||||
@ -34,6 +35,11 @@ pub const Mouse = struct {
|
|||||||
/// viewport points to avoid the complexity of mapping the mouse to
|
/// viewport points to avoid the complexity of mapping the mouse to
|
||||||
/// the renderer state.
|
/// the renderer state.
|
||||||
point: ?terminal.point.Viewport = null,
|
point: ?terminal.point.Viewport = null,
|
||||||
|
|
||||||
|
/// The mods that are currently active for the last mouse event.
|
||||||
|
/// This could really just be mods in general and we probably will
|
||||||
|
/// move it out of mouse state at some point.
|
||||||
|
mods: inputpkg.Mods = .{},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// The pre-edit state. See Surface.preeditCallback for more information.
|
/// The pre-edit state. See Surface.preeditCallback for more information.
|
||||||
|
@ -65,6 +65,7 @@ pub const Set = struct {
|
|||||||
alloc: Allocator,
|
alloc: Allocator,
|
||||||
screen: *Screen,
|
screen: *Screen,
|
||||||
mouse_vp_pt: point.Viewport,
|
mouse_vp_pt: point.Viewport,
|
||||||
|
mouse_mods: inputpkg.Mods,
|
||||||
) !MatchSet {
|
) !MatchSet {
|
||||||
// Convert the viewport point to a screen point.
|
// Convert the viewport point to a screen point.
|
||||||
const mouse_pt = mouse_vp_pt.toScreen(screen);
|
const mouse_pt = mouse_vp_pt.toScreen(screen);
|
||||||
@ -90,10 +91,18 @@ pub const Set = struct {
|
|||||||
|
|
||||||
// Go through each link and see if we have any matches.
|
// Go through each link and see if we have any matches.
|
||||||
for (self.links) |link| {
|
for (self.links) |link| {
|
||||||
// If this is a hover link and our mouse point isn't within
|
// Determine if our highlight conditions are met. We use a
|
||||||
// this line at all, we can skip it.
|
// switch here instead of an if so that we can get a compile
|
||||||
if (link.highlight == .hover) {
|
// error if any other conditions are added.
|
||||||
if (!line.selection().contains(mouse_pt)) continue;
|
switch (link.highlight) {
|
||||||
|
.always => {},
|
||||||
|
.always_mods => |v| if (!mouse_mods.equal(v)) continue,
|
||||||
|
inline .hover, .hover_mods => |v, tag| {
|
||||||
|
if (!line.selection().contains(mouse_pt)) continue;
|
||||||
|
if (comptime tag == .hover_mods) {
|
||||||
|
if (!mouse_mods.equal(v)) continue;
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var it = strmap.searchIterator(link.regex);
|
var it = strmap.searchIterator(link.regex);
|
||||||
@ -186,7 +195,7 @@ test "matchset" {
|
|||||||
defer set.deinit(alloc);
|
defer set.deinit(alloc);
|
||||||
|
|
||||||
// Get our matches
|
// Get our matches
|
||||||
var match = try set.matchSet(alloc, &s, .{});
|
var match = try set.matchSet(alloc, &s, .{}, .{});
|
||||||
defer match.deinit(alloc);
|
defer match.deinit(alloc);
|
||||||
try testing.expectEqual(@as(usize, 2), match.matches.len);
|
try testing.expectEqual(@as(usize, 2), match.matches.len);
|
||||||
|
|
||||||
@ -227,7 +236,7 @@ test "matchset hover links" {
|
|||||||
|
|
||||||
// Not hovering over the first link
|
// Not hovering over the first link
|
||||||
{
|
{
|
||||||
var match = try set.matchSet(alloc, &s, .{});
|
var match = try set.matchSet(alloc, &s, .{}, .{});
|
||||||
defer match.deinit(alloc);
|
defer match.deinit(alloc);
|
||||||
try testing.expectEqual(@as(usize, 1), match.matches.len);
|
try testing.expectEqual(@as(usize, 1), match.matches.len);
|
||||||
|
|
||||||
@ -242,7 +251,7 @@ test "matchset hover links" {
|
|||||||
|
|
||||||
// Hovering over the first link
|
// Hovering over the first link
|
||||||
{
|
{
|
||||||
var match = try set.matchSet(alloc, &s, .{ .x = 1, .y = 0 });
|
var match = try set.matchSet(alloc, &s, .{ .x = 1, .y = 0 }, .{});
|
||||||
defer match.deinit(alloc);
|
defer match.deinit(alloc);
|
||||||
try testing.expectEqual(@as(usize, 2), match.matches.len);
|
try testing.expectEqual(@as(usize, 2), match.matches.len);
|
||||||
|
|
||||||
@ -255,3 +264,43 @@ test "matchset hover links" {
|
|||||||
try testing.expect(!match.orderedContains(.{ .x = 1, .y = 2 }));
|
try testing.expect(!match.orderedContains(.{ .x = 1, .y = 2 }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "matchset mods no match" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
// Initialize our screen
|
||||||
|
var s = try Screen.init(alloc, 5, 5, 0);
|
||||||
|
defer s.deinit();
|
||||||
|
const str = "1ABCD2EFGH\n3IJKL";
|
||||||
|
try s.testWriteString(str);
|
||||||
|
|
||||||
|
// Get a set
|
||||||
|
var set = try Set.fromConfig(alloc, &.{
|
||||||
|
.{
|
||||||
|
.regex = "AB",
|
||||||
|
.action = .{ .open = {} },
|
||||||
|
.highlight = .{ .always = {} },
|
||||||
|
},
|
||||||
|
|
||||||
|
.{
|
||||||
|
.regex = "EF",
|
||||||
|
.action = .{ .open = {} },
|
||||||
|
.highlight = .{ .always_mods = .{ .ctrl = true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
defer set.deinit(alloc);
|
||||||
|
|
||||||
|
// Get our matches
|
||||||
|
var match = try set.matchSet(alloc, &s, .{}, .{});
|
||||||
|
defer match.deinit(alloc);
|
||||||
|
try testing.expectEqual(@as(usize, 1), match.matches.len);
|
||||||
|
|
||||||
|
// Test our matches
|
||||||
|
try testing.expect(!match.orderedContains(.{ .x = 0, .y = 0 }));
|
||||||
|
try testing.expect(match.orderedContains(.{ .x = 1, .y = 0 }));
|
||||||
|
try testing.expect(match.orderedContains(.{ .x = 2, .y = 0 }));
|
||||||
|
try testing.expect(!match.orderedContains(.{ .x = 3, .y = 0 }));
|
||||||
|
try testing.expect(!match.orderedContains(.{ .x = 1, .y = 1 }));
|
||||||
|
try testing.expect(!match.orderedContains(.{ .x = 1, .y = 2 }));
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user