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 {
|
||||
regex: oni.Regex,
|
||||
action: input.Link.Action,
|
||||
highlight: input.Link.Highlight,
|
||||
};
|
||||
|
||||
pub fn init(alloc_gpa: Allocator, config: *const configpkg.Config) !DerivedConfig {
|
||||
@ -215,6 +216,7 @@ const DerivedConfig = struct {
|
||||
try links.append(.{
|
||||
.regex = regex,
|
||||
.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.
|
||||
fn updateRendererHealth(self: *Surface, health: renderer.Health) void {
|
||||
log.warn("renderer health status change status={}", .{health});
|
||||
@ -1352,10 +1383,12 @@ pub fn keyCallback(
|
||||
// to hide it again if it was 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
|
||||
// mod changes can affect link highlighting.
|
||||
self.mouse.link_point = null;
|
||||
self.mouse.mods = event.mods;
|
||||
const pos = self.rt_surface.getCursorPos() catch break :mouse_mods;
|
||||
self.cursorPosCallback(pos) catch {};
|
||||
if (rehide) self.hideMouse();
|
||||
@ -1969,11 +2002,13 @@ pub fn mouseButtonCallback(
|
||||
|
||||
// Always record our latest mouse state
|
||||
self.mouse.click_state[@intCast(@intFromEnum(button))] = action;
|
||||
self.mouse.mods = @bitCast(mods);
|
||||
|
||||
// Always show the mouse again if it is hidden
|
||||
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
|
||||
// modifer. Note we can do this more efficiently probably with less
|
||||
// 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
|
||||
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);
|
||||
while (true) {
|
||||
var match = (try it.next()) orelse break;
|
||||
|
@ -432,8 +432,9 @@ command: ?[]const u8 = null,
|
||||
/// TODO: This can't currently be set!
|
||||
link: RepeatableLink = .{},
|
||||
|
||||
/// Enable URL matching. URLs are matched on hover and open using the default
|
||||
/// system application for the linked URL.
|
||||
/// Enable URL matching. URLs are matched on hover with control (Linux) or
|
||||
/// 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
|
||||
/// `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, .{
|
||||
.regex = url.regex,
|
||||
.action = .{ .open = {} },
|
||||
.highlight = .{ .hover = {} },
|
||||
.highlight = .{ .hover_mods = inputpkg.ctrlOrSuper(.{}) },
|
||||
});
|
||||
|
||||
return result;
|
||||
|
@ -5,6 +5,7 @@
|
||||
const Link = @This();
|
||||
|
||||
const oni = @import("oniguruma");
|
||||
const Mods = @import("key.zig").Mods;
|
||||
|
||||
/// 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.
|
||||
@ -30,6 +31,13 @@ pub const Highlight = union(enum) {
|
||||
|
||||
/// Only highlight the link when the mouse is hovering over it.
|
||||
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.
|
||||
|
@ -1555,6 +1555,7 @@ fn rebuildCells(
|
||||
arena_alloc,
|
||||
screen,
|
||||
mouse.point orelse .{},
|
||||
mouse.mods,
|
||||
);
|
||||
|
||||
// Determine our x/y range for preedit. We don't want to render anything
|
||||
|
@ -991,6 +991,7 @@ pub fn rebuildCells(
|
||||
arena_alloc,
|
||||
screen,
|
||||
mouse.point orelse .{},
|
||||
mouse.mods,
|
||||
);
|
||||
|
||||
// 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 Inspector = @import("../inspector/main.zig").Inspector;
|
||||
const terminal = @import("../terminal/main.zig");
|
||||
const inputpkg = @import("../input.zig");
|
||||
const renderer = @import("../renderer.zig");
|
||||
|
||||
/// 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
|
||||
/// the renderer state.
|
||||
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.
|
||||
|
@ -65,6 +65,7 @@ pub const Set = struct {
|
||||
alloc: Allocator,
|
||||
screen: *Screen,
|
||||
mouse_vp_pt: point.Viewport,
|
||||
mouse_mods: inputpkg.Mods,
|
||||
) !MatchSet {
|
||||
// Convert the viewport point to a screen point.
|
||||
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.
|
||||
for (self.links) |link| {
|
||||
// If this is a hover link and our mouse point isn't within
|
||||
// this line at all, we can skip it.
|
||||
if (link.highlight == .hover) {
|
||||
if (!line.selection().contains(mouse_pt)) continue;
|
||||
// Determine if our highlight conditions are met. We use a
|
||||
// switch here instead of an if so that we can get a compile
|
||||
// error if any other conditions are added.
|
||||
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);
|
||||
@ -186,7 +195,7 @@ test "matchset" {
|
||||
defer set.deinit(alloc);
|
||||
|
||||
// Get our matches
|
||||
var match = try set.matchSet(alloc, &s, .{});
|
||||
var match = try set.matchSet(alloc, &s, .{}, .{});
|
||||
defer match.deinit(alloc);
|
||||
try testing.expectEqual(@as(usize, 2), match.matches.len);
|
||||
|
||||
@ -227,7 +236,7 @@ test "matchset hover links" {
|
||||
|
||||
// Not hovering over the first link
|
||||
{
|
||||
var match = try set.matchSet(alloc, &s, .{});
|
||||
var match = try set.matchSet(alloc, &s, .{}, .{});
|
||||
defer match.deinit(alloc);
|
||||
try testing.expectEqual(@as(usize, 1), match.matches.len);
|
||||
|
||||
@ -242,7 +251,7 @@ test "matchset hover links" {
|
||||
|
||||
// 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);
|
||||
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 }));
|
||||
}
|
||||
}
|
||||
|
||||
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