Merge pull request #1387 from mattrobenolt/cmd-click

Open links with Super+click
This commit is contained in:
Mitchell Hashimoto
2024-01-27 21:04:51 -08:00
committed by GitHub
7 changed files with 118 additions and 12 deletions

View File

@ -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;

View File

@ -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;

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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) {
// 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 }));
}