mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-10-07 02:16:34 +00:00
add a new highlight state that requires modifiers
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
|
||||||
@@ -2008,7 +2043,7 @@ pub fn mouseButtonCallback(
|
|||||||
// Handle link clicking. We want to do this before we do mouse
|
// Handle link clicking. We want to do this before we do mouse
|
||||||
// reporting or any other mouse handling because a successfully
|
// reporting or any other mouse handling because a successfully
|
||||||
// clicked link will swallow the event.
|
// clicked link will swallow the event.
|
||||||
if (button == .left and action == .release and mods.ctrlOrSuper() and self.mouse.over_link) {
|
if (button == .left and action == .release and self.mouse.over_link) {
|
||||||
const pos = try self.rt_surface.getCursorPos();
|
const pos = try self.rt_surface.getCursorPos();
|
||||||
if (self.processLinks(pos)) |processed| {
|
if (self.processLinks(pos)) |processed| {
|
||||||
if (processed) return;
|
if (processed) return;
|
||||||
@@ -2238,9 +2273,6 @@ fn linkAtPos(
|
|||||||
// If we have no configured links we can save a lot of work
|
// If we have no configured links we can save a lot of work
|
||||||
if (self.config.links.len == 0) return null;
|
if (self.config.links.len == 0) return null;
|
||||||
|
|
||||||
// Require super to be held down, so bail early
|
|
||||||
if (!self.mouse.mods.ctrlOrSuper()) return null;
|
|
||||||
|
|
||||||
// Convert our cursor position to a screen point.
|
// Convert our cursor position to a screen point.
|
||||||
const mouse_pt = mouse_pt: {
|
const mouse_pt = mouse_pt: {
|
||||||
const viewport_point = self.posToViewport(pos.x, pos.y);
|
const viewport_point = self.posToViewport(pos.x, pos.y);
|
||||||
@@ -2255,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 => {},
|
||||||
|
.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;
|
||||||
|
@@ -1437,7 +1437,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 = .{ .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,10 @@ 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, regardless of
|
||||||
|
/// hover state.
|
||||||
|
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
|
||||||
|
@@ -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,13 @@ 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 => {},
|
||||||
|
.hover => if (!line.selection().contains(mouse_pt)) continue,
|
||||||
|
.mods => |v| if (!mouse_mods.equal(v)) continue,
|
||||||
}
|
}
|
||||||
|
|
||||||
var it = strmap.searchIterator(link.regex);
|
var it = strmap.searchIterator(link.regex);
|
||||||
@@ -186,7 +190,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 +231,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 +246,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 +259,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 = .{ .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