mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 07:46:12 +03:00
renderer/metal: underline urls
This commit is contained in:
@ -4,6 +4,7 @@ pub usingnamespace @import("regex.zig");
|
||||
pub usingnamespace @import("region.zig");
|
||||
pub usingnamespace @import("types.zig");
|
||||
pub const c = @import("c.zig");
|
||||
pub const testing = @import("testing.zig");
|
||||
|
||||
test {
|
||||
@import("std").testing.refAllDecls(@This());
|
||||
|
@ -3,6 +3,7 @@ const builtin = @import("builtin");
|
||||
pub usingnamespace @import("config/key.zig");
|
||||
pub const Config = @import("config/Config.zig");
|
||||
pub const string = @import("config/string.zig");
|
||||
pub const url = @import("config/url.zig");
|
||||
|
||||
// Field types
|
||||
pub const CopyOnSelect = Config.CopyOnSelect;
|
||||
|
26
src/config/url.zig
Normal file
26
src/config/url.zig
Normal file
@ -0,0 +1,26 @@
|
||||
const std = @import("std");
|
||||
const oni = @import("oniguruma");
|
||||
|
||||
/// Default URL regex. This is used to detect URLs in terminal output.
|
||||
/// This is here in the config package because one day the matchers will be
|
||||
/// configurable and this will be a default.
|
||||
///
|
||||
/// This is taken from the Alacritty project.
|
||||
pub const regex = "(ipfs:|ipns:|magnet:|mailto:|gemini://|gopher://|https://|http://|news:|file:|git://|ssh:|ftp://)[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>\x22\\s{-}\\^⟨⟩\x60]+";
|
||||
|
||||
test "url regex" {
|
||||
try oni.testing.ensureInit();
|
||||
var re = try oni.Regex.init(regex, .{}, oni.Encoding.utf8, oni.Syntax.default, null);
|
||||
defer re.deinit();
|
||||
|
||||
// The URL cases to test that our regex matches. Feel free to add to this
|
||||
// as we find bugs or just want more coverage.
|
||||
const cases: []const []const u8 = &.{
|
||||
"https://example.com",
|
||||
};
|
||||
|
||||
for (cases) |case| {
|
||||
var reg = try re.search(case, .{});
|
||||
defer reg.deinit();
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ const options = @import("build_options");
|
||||
const glfw = @import("glfw");
|
||||
const glslang = @import("glslang");
|
||||
const macos = @import("macos");
|
||||
const oni = @import("oniguruma");
|
||||
const tracy = @import("tracy");
|
||||
const cli = @import("cli.zig");
|
||||
const internal_os = @import("os/main.zig");
|
||||
@ -277,6 +278,9 @@ pub const GlobalState = struct {
|
||||
// Initialize glslang for shader compilation
|
||||
try glslang.init();
|
||||
|
||||
// Initialize oniguruma for regex
|
||||
try oni.init(&.{oni.Encoding.utf8});
|
||||
|
||||
// Find our resources directory once for the app so every launch
|
||||
// hereafter can use this cached value.
|
||||
self.resources_dir = try internal_os.resourcesDir(self.alloc);
|
||||
|
@ -9,6 +9,7 @@ const builtin = @import("builtin");
|
||||
const glfw = @import("glfw");
|
||||
const objc = @import("objc");
|
||||
const macos = @import("macos");
|
||||
const oni = @import("oniguruma");
|
||||
const imgui = @import("imgui");
|
||||
const glslang = @import("glslang");
|
||||
const apprt = @import("../apprt.zig");
|
||||
@ -119,6 +120,11 @@ texture_color: objc.Object, // MTLTexture
|
||||
/// Custom shader state. This is only set if we have custom shaders.
|
||||
custom_shader_state: ?CustomShaderState = null,
|
||||
|
||||
/// Our URL regex.
|
||||
/// One day, this will be part of DerivedConfig since all regex matchers
|
||||
/// will be part of the config.
|
||||
url_regex: oni.Regex,
|
||||
|
||||
pub const CustomShaderState = struct {
|
||||
/// The screen texture that we render the terminal to. If we don't have
|
||||
/// custom shaders, we render directly to the drawable.
|
||||
@ -342,6 +348,16 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
|
||||
const texture_greyscale = try initAtlasTexture(device, &options.font_group.atlas_greyscale);
|
||||
const texture_color = try initAtlasTexture(device, &options.font_group.atlas_color);
|
||||
|
||||
// Create our URL regex
|
||||
var url_re = try oni.Regex.init(
|
||||
configpkg.url.regex,
|
||||
.{},
|
||||
oni.Encoding.utf8,
|
||||
oni.Syntax.default,
|
||||
null,
|
||||
);
|
||||
errdefer url_re.deinit();
|
||||
|
||||
return Metal{
|
||||
.alloc = alloc,
|
||||
.config = options.config,
|
||||
@ -382,6 +398,8 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
|
||||
.texture_greyscale = texture_greyscale,
|
||||
.texture_color = texture_color,
|
||||
.custom_shader_state = custom_shader_state,
|
||||
|
||||
.url_regex = url_re,
|
||||
};
|
||||
}
|
||||
|
||||
@ -411,6 +429,8 @@ pub fn deinit(self: *Metal) void {
|
||||
|
||||
self.shaders.deinit(self.alloc);
|
||||
|
||||
self.url_regex.deinit();
|
||||
|
||||
self.* = undefined;
|
||||
}
|
||||
|
||||
@ -1371,6 +1391,49 @@ fn rebuildCells(
|
||||
(screen.rows * screen.cols * 2) + 1,
|
||||
);
|
||||
|
||||
// Create an arena for all our temporary allocations while rebuilding
|
||||
var arena = ArenaAllocator.init(self.alloc);
|
||||
defer arena.deinit();
|
||||
const arena_alloc = arena.allocator();
|
||||
|
||||
// All of our matching URLs. The selections in this list MUST be in
|
||||
// order of left-to-right top-to-bottom (English reading order). This
|
||||
// requirement is necessary for the URL highlighting to work properly
|
||||
// and has nothing to do with the locale.
|
||||
var urls = std.ArrayList(terminal.Selection).init(arena_alloc);
|
||||
|
||||
// Find all the cells that have URLs.
|
||||
var lineIter = screen.lineIterator(.viewport);
|
||||
while (lineIter.next()) |line| {
|
||||
const strmap = line.stringMap(arena_alloc) catch continue;
|
||||
defer strmap.deinit(arena_alloc);
|
||||
|
||||
var offset: usize = 0;
|
||||
while (true) {
|
||||
var match = self.url_regex.search(strmap.string[offset..], .{}) catch |err| {
|
||||
switch (err) {
|
||||
error.Mismatch => {},
|
||||
else => log.warn("failed to search for URLs err={}", .{err}),
|
||||
}
|
||||
|
||||
break;
|
||||
};
|
||||
defer match.deinit();
|
||||
|
||||
// Determine the screen point for the match
|
||||
const start_idx: usize = @intCast(match.starts()[0]);
|
||||
const end_idx: usize = @intCast(match.ends()[0] - 1);
|
||||
const start_pt = strmap.map[offset + start_idx];
|
||||
const end_pt = strmap.map[offset + end_idx];
|
||||
|
||||
// Move our offset so we can continue searching
|
||||
offset += end_idx;
|
||||
|
||||
// Store our selection
|
||||
try urls.append(.{ .start = start_pt, .end = end_pt });
|
||||
}
|
||||
}
|
||||
|
||||
// Determine our x/y range for preedit. We don't want to render anything
|
||||
// here because we will render the preedit separately.
|
||||
const preedit_range: ?struct {
|
||||
@ -1388,6 +1451,10 @@ fn rebuildCells(
|
||||
// remains visible.
|
||||
var cursor_cell: ?mtl_shaders.Cell = null;
|
||||
|
||||
// Keep track of our current hint that is being tracked.
|
||||
var hint: ?terminal.Selection = if (urls.items.len > 0) urls.items[0] else null;
|
||||
var hint_i: usize = 0;
|
||||
|
||||
// Build each cell
|
||||
var rowIter = screen.rowIterator(.viewport);
|
||||
var y: usize = 0;
|
||||
@ -1475,10 +1542,43 @@ fn rebuildCells(
|
||||
}
|
||||
}
|
||||
|
||||
// It this cell is within our hint range then we need to
|
||||
// underline it.
|
||||
const cell: terminal.Screen.Cell = cell: {
|
||||
var cell = row.getCell(shaper_cell.x);
|
||||
if (hint) |sel| hint: {
|
||||
const pt: terminal.point.ScreenPoint = .{
|
||||
.x = shaper_cell.x,
|
||||
.y = y,
|
||||
};
|
||||
|
||||
// If the end is before the point then we try to
|
||||
// move to the next hint.
|
||||
var compare_sel: terminal.Selection = sel;
|
||||
if (sel.end.before(pt)) {
|
||||
hint_i += 1;
|
||||
if (hint_i >= urls.items.len) {
|
||||
hint = null;
|
||||
break :hint;
|
||||
}
|
||||
|
||||
compare_sel = urls.items[hint_i];
|
||||
hint = compare_sel;
|
||||
}
|
||||
|
||||
if (compare_sel.contains(pt)) {
|
||||
cell.attrs.underline = .single;
|
||||
break :hint;
|
||||
}
|
||||
}
|
||||
|
||||
break :cell cell;
|
||||
};
|
||||
|
||||
if (self.updateCell(
|
||||
term_selection,
|
||||
screen,
|
||||
row.getCell(shaper_cell.x),
|
||||
cell,
|
||||
shaper_cell,
|
||||
run,
|
||||
shaper_cell.x,
|
||||
|
Reference in New Issue
Block a user