Merge pull request #968 from mitchellh/urls

Clickable URLs
This commit is contained in:
Mitchell Hashimoto
2023-11-30 12:15:55 -08:00
committed by GitHub
31 changed files with 1830 additions and 113 deletions

View File

@ -686,6 +686,10 @@ fn addDeps(
.target = step.target,
.optimize = step.optimize,
});
const oniguruma_dep = b.dependency("oniguruma", .{
.target = step.target,
.optimize = step.optimize,
});
const opengl_dep = b.dependency("opengl", .{});
const pixman_dep = b.dependency("pixman", .{
.target = step.target,
@ -749,6 +753,7 @@ fn addDeps(
"fontconfig",
fontconfig_dep.module("fontconfig"),
);
step.addModule("oniguruma", oniguruma_dep.module("oniguruma"));
step.addModule("freetype", freetype_dep.module("freetype"));
step.addModule("glslang", glslang_dep.module("glslang"));
step.addModule("spirv_cross", spirv_cross_dep.module("spirv_cross"));
@ -793,6 +798,7 @@ fn addDeps(
step.linkSystemLibrary2("freetype2", dynamic_link_opts);
step.linkSystemLibrary2("harfbuzz", dynamic_link_opts);
step.linkSystemLibrary2("libpng", dynamic_link_opts);
step.linkSystemLibrary2("oniguruma", dynamic_link_opts);
step.linkSystemLibrary2("pixman-1", dynamic_link_opts);
step.linkSystemLibrary2("zlib", dynamic_link_opts);
@ -803,6 +809,9 @@ fn addDeps(
// Other dependencies, we may dynamically link
if (static) {
step.linkLibrary(oniguruma_dep.artifact("oniguruma"));
try static_libs.append(oniguruma_dep.artifact("oniguruma").getEmittedBin());
step.linkLibrary(zlib_dep.artifact("z"));
try static_libs.append(zlib_dep.artifact("z").getEmittedBin());

View File

@ -32,6 +32,7 @@
.harfbuzz = .{ .path = "./pkg/harfbuzz" },
.libpng = .{ .path = "./pkg/libpng" },
.macos = .{ .path = "./pkg/macos" },
.oniguruma = .{ .path = "./pkg/oniguruma" },
.opengl = .{ .path = "./pkg/opengl" },
.pixman = .{ .path = "./pkg/pixman" },
.tracy = .{ .path = "./pkg/tracy" },

View File

@ -6,6 +6,7 @@
, ncurses
, nodejs
, nodePackages
, oniguruma
, parallel
, pkg-config
, python3
@ -52,6 +53,7 @@ let
freetype
harfbuzz
libpng
oniguruma
pixman
zlib
@ -111,6 +113,7 @@ in mkShell rec {
freetype
harfbuzz
libpng
oniguruma
pixman
zlib

View File

@ -1,3 +1,3 @@
# This file is auto-generated! check build-support/check-zig-cache-hash.sh for
# more details.
"sha256-KMv4j1/UxsrRPkr+Fy9BjtzwFZquiLfJCLMP28oVZiU="
"sha256-bQ81e0vJBSLkVJ3a3nLraVc/JeG77xGckuARmu81DqI="

130
pkg/oniguruma/build.zig Normal file
View File

@ -0,0 +1,130 @@
const std = @import("std");
const NativeTargetInfo = std.zig.system.NativeTargetInfo;
pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
_ = b.addModule("oniguruma", .{ .source_file = .{ .path = "main.zig" } });
const upstream = b.dependency("oniguruma", .{});
const lib = try buildOniguruma(b, upstream, target, optimize);
b.installArtifact(lib);
{
const test_exe = b.addTest(.{
.name = "test",
.root_source_file = .{ .path = "main.zig" },
.target = target,
.optimize = optimize,
});
test_exe.linkLibrary(lib);
const tests_run = b.addRunArtifact(test_exe);
const test_step = b.step("test", "Run tests");
test_step.dependOn(&tests_run.step);
// Uncomment this if we're debugging tests
b.installArtifact(test_exe);
}
}
fn buildOniguruma(
b: *std.Build,
upstream: *std.Build.Dependency,
target: std.zig.CrossTarget,
optimize: std.builtin.OptimizeMode,
) !*std.Build.Step.Compile {
const lib = b.addStaticLibrary(.{
.name = "oniguruma",
.target = target,
.optimize = optimize,
});
const t = lib.target_info.target;
lib.linkLibC();
lib.addIncludePath(upstream.path("src"));
lib.addConfigHeader(b.addConfigHeader(.{
.style = .{ .cmake = upstream.path("src/config.h.cmake.in") },
}, .{
.PACKAGE = "oniguruma",
.PACKAGE_VERSION = "6.9.9",
.VERSION = "6.9.9",
.HAVE_STDINT_H = true,
.HAVE_SYS_TIMES_H = true,
.HAVE_SYS_TIME_H = true,
.HAVE_SYS_TYPES_H = true,
.HAVE_UNISTD_H = true,
.HAVE_INTTYPES_H = true,
.SIZEOF_INT = t.c_type_byte_size(.int),
.SIZEOF_LONG = t.c_type_byte_size(.long),
.SIZEOF_LONG_LONG = t.c_type_byte_size(.longlong),
.SIZEOF_VOIDP = t.ptrBitWidth() / t.c_type_bit_size(.char),
}));
var flags = std.ArrayList([]const u8).init(b.allocator);
defer flags.deinit();
try flags.appendSlice(&.{});
lib.addCSourceFiles(.{
.dependency = upstream,
.flags = flags.items,
.files = &.{
"src/regerror.c",
"src/regparse.c",
"src/regext.c",
"src/regcomp.c",
"src/regexec.c",
"src/reggnu.c",
"src/regenc.c",
"src/regsyntax.c",
"src/regtrav.c",
"src/regversion.c",
"src/st.c",
"src/onig_init.c",
"src/unicode.c",
"src/ascii.c",
"src/utf8.c",
"src/utf16_be.c",
"src/utf16_le.c",
"src/utf32_be.c",
"src/utf32_le.c",
"src/euc_jp.c",
"src/sjis.c",
"src/iso8859_1.c",
"src/iso8859_2.c",
"src/iso8859_3.c",
"src/iso8859_4.c",
"src/iso8859_5.c",
"src/iso8859_6.c",
"src/iso8859_7.c",
"src/iso8859_8.c",
"src/iso8859_9.c",
"src/iso8859_10.c",
"src/iso8859_11.c",
"src/iso8859_13.c",
"src/iso8859_14.c",
"src/iso8859_15.c",
"src/iso8859_16.c",
"src/euc_tw.c",
"src/euc_kr.c",
"src/big5.c",
"src/gb18030.c",
"src/koi8_r.c",
"src/cp1251.c",
"src/euc_jp_prop.c",
"src/sjis_prop.c",
"src/unicode_unfold_key.c",
"src/unicode_fold1_key.c",
"src/unicode_fold2_key.c",
"src/unicode_fold3_key.c",
},
});
lib.installHeadersDirectoryOptions(.{
.source_dir = upstream.path("src"),
.install_dir = .header,
.install_subdir = "",
.include_extensions = &.{".h"},
});
return lib;
}

View File

@ -0,0 +1,11 @@
.{
.name = "oniguruma",
.version = "6.9.9",
.paths = .{""},
.dependencies = .{
.oniguruma = .{
.url = "https://github.com/kkos/oniguruma/archive/refs/tags/v6.9.9.tar.gz",
.hash = "1220c15e72eadd0d9085a8af134904d9a0f5dfcbed5f606ad60edc60ebeccd9706bb",
},
},
}

3
pkg/oniguruma/c.zig Normal file
View File

@ -0,0 +1,3 @@
pub usingnamespace @cImport({
@cInclude("oniguruma.h");
});

201
pkg/oniguruma/errors.zig Normal file
View File

@ -0,0 +1,201 @@
const c = @import("c.zig");
const Encoding = @import("types.zig").Encoding;
/// Maximum error message length.
pub const MAX_ERROR_LEN = c.ONIG_MAX_ERROR_MESSAGE_LEN;
/// Convert an Oniguruma error to an error.
pub fn convertError(code: c_int) !c_int {
if (code >= 0) return code;
inline for (error_code_map) |m| {
if (m[1] == code) return m[0];
}
return Error.Unknown;
}
/// Convert an error code to a string. buf must be at least
/// MAX_ERROR_LEN bytes long.
pub fn errorString(buf: []u8, code: c_int) ![]u8 {
const len = c.onig_error_code_to_str(buf.ptr, code);
return buf[0..@intCast(len)];
}
/// The Oniguruma error info type, matching the C type exactly.
pub const ErrorInfo = extern struct {
encoding: *Encoding,
par: [*]u8,
par_end: [*]u8,
};
/// All possible Oniguruma errors.
pub const Error = error{
Mismatch,
NoSupportConfig,
Abort,
Memory,
TypeBug,
ParserBug,
StackBug,
UndefinedBytecode,
UnexpectedBytecode,
MatchStackLimitOver,
ParseDepthLimitOver,
RetryLimitInMatchOver,
RetryLimitInSearchOver,
SubexpCallLimitInSearchOver,
DefaultEncodingIsNotSet,
SpecifiedEncodingCantConvertToWideChar,
FailToInitialize,
InvalidArgument,
EndPatternAtLeftBrace,
EndPatternAtLeftBracket,
EmptyCharClass,
PrematureEndOfCharClass,
EndPatternAtEscape,
EndPatternAtMeta,
EndPatternAtControl,
MetaCodeSyntax,
ControlCodeSyntax,
CharClassValueAtEndOfRange,
CharClassValueAtStartOfRange,
UnmatchedRangeSpecifierInCharClass,
TargetOfRepeatOperatorNotSpecified,
TargetOfRepeatOperatorInvalid,
NestedRepeatOperator,
UnmatchedCloseParenthesis,
EndPatternWithUnmatchedParenthesis,
EndPatternInGroup,
UndefinedGroupOption,
InvalidGroupOption,
InvalidPosixBracketType,
InvalidLookBehindPattern,
InvalidRepeatRangePattern,
TooBigNumber,
TooBigNumberForRepeatRange,
UpperSmallerThanLowerInRepeatRange,
EmptyRangeInCharClass,
MismatchCodeLengthInClassRange,
TooManyMultiByteRanges,
TooShortMultiByteString,
TooBigBackrefNumber,
InvalidBackref,
NumberedBackrefOrCallNotAllowed,
TooManyCaptures,
TooLongWideCharValue,
UndefinedOperator,
EmptyGroupName,
InvalidGroupName,
InvalidCharInGroupName,
UndefinedNameReference,
UndefinedGroupReference,
MultiplexDefinedName,
MultiplexDefinitionNameCall,
NeverEndingRecursion,
GroupNumberOverForCaptureHistory,
InvalidCharPropertyName,
InvalidIfElseSyntax,
InvalidAbsentGroupPattern,
InvalidAbsentGroupGeneratorPattern,
InvalidCalloutPattern,
InvalidCalloutName,
UndefinedCalloutName,
InvalidCalloutBody,
InvalidCalloutTagName,
InvalidCalloutArg,
InvalidCodePointValue,
InvalidWideCharValue,
TooBigWideCharValue,
NotSupportedEncodingCombination,
InvalidCombinationOfOptions,
TooManyUserDefinedObjects,
TooLongPropertyName,
VeryInefficientPattern,
LibraryIsNotInitialized,
Unknown,
};
const error_code_map: []const struct { Error, c_int } = &.{
.{ Error.Mismatch, c.ONIG_MISMATCH },
.{ Error.NoSupportConfig, c.ONIG_NO_SUPPORT_CONFIG },
.{ Error.Abort, c.ONIG_ABORT },
.{ Error.Memory, c.ONIGERR_MEMORY },
.{ Error.TypeBug, c.ONIGERR_TYPE_BUG },
.{ Error.ParserBug, c.ONIGERR_PARSER_BUG },
.{ Error.StackBug, c.ONIGERR_STACK_BUG },
.{ Error.UndefinedBytecode, c.ONIGERR_UNDEFINED_BYTECODE },
.{ Error.UnexpectedBytecode, c.ONIGERR_UNEXPECTED_BYTECODE },
.{ Error.MatchStackLimitOver, c.ONIGERR_MATCH_STACK_LIMIT_OVER },
.{ Error.ParseDepthLimitOver, c.ONIGERR_PARSE_DEPTH_LIMIT_OVER },
.{ Error.RetryLimitInMatchOver, c.ONIGERR_RETRY_LIMIT_IN_MATCH_OVER },
.{ Error.RetryLimitInSearchOver, c.ONIGERR_RETRY_LIMIT_IN_SEARCH_OVER },
.{ Error.SubexpCallLimitInSearchOver, c.ONIGERR_SUBEXP_CALL_LIMIT_IN_SEARCH_OVER },
.{ Error.DefaultEncodingIsNotSet, c.ONIGERR_DEFAULT_ENCODING_IS_NOT_SET },
.{ Error.SpecifiedEncodingCantConvertToWideChar, c.ONIGERR_SPECIFIED_ENCODING_CANT_CONVERT_TO_WIDE_CHAR },
.{ Error.FailToInitialize, c.ONIGERR_FAIL_TO_INITIALIZE },
.{ Error.InvalidArgument, c.ONIGERR_INVALID_ARGUMENT },
.{ Error.EndPatternAtLeftBrace, c.ONIGERR_END_PATTERN_AT_LEFT_BRACE },
.{ Error.EndPatternAtLeftBracket, c.ONIGERR_END_PATTERN_AT_LEFT_BRACKET },
.{ Error.EmptyCharClass, c.ONIGERR_EMPTY_CHAR_CLASS },
.{ Error.PrematureEndOfCharClass, c.ONIGERR_PREMATURE_END_OF_CHAR_CLASS },
.{ Error.EndPatternAtEscape, c.ONIGERR_END_PATTERN_AT_ESCAPE },
.{ Error.EndPatternAtMeta, c.ONIGERR_END_PATTERN_AT_META },
.{ Error.EndPatternAtControl, c.ONIGERR_END_PATTERN_AT_CONTROL },
.{ Error.MetaCodeSyntax, c.ONIGERR_META_CODE_SYNTAX },
.{ Error.ControlCodeSyntax, c.ONIGERR_CONTROL_CODE_SYNTAX },
.{ Error.CharClassValueAtEndOfRange, c.ONIGERR_CHAR_CLASS_VALUE_AT_END_OF_RANGE },
.{ Error.CharClassValueAtStartOfRange, c.ONIGERR_CHAR_CLASS_VALUE_AT_START_OF_RANGE },
.{ Error.UnmatchedRangeSpecifierInCharClass, c.ONIGERR_UNMATCHED_RANGE_SPECIFIER_IN_CHAR_CLASS },
.{ Error.TargetOfRepeatOperatorNotSpecified, c.ONIGERR_TARGET_OF_REPEAT_OPERATOR_NOT_SPECIFIED },
.{ Error.TargetOfRepeatOperatorInvalid, c.ONIGERR_TARGET_OF_REPEAT_OPERATOR_INVALID },
.{ Error.NestedRepeatOperator, c.ONIGERR_NESTED_REPEAT_OPERATOR },
.{ Error.UnmatchedCloseParenthesis, c.ONIGERR_UNMATCHED_CLOSE_PARENTHESIS },
.{ Error.EndPatternWithUnmatchedParenthesis, c.ONIGERR_END_PATTERN_WITH_UNMATCHED_PARENTHESIS },
.{ Error.EndPatternInGroup, c.ONIGERR_END_PATTERN_IN_GROUP },
.{ Error.UndefinedGroupOption, c.ONIGERR_UNDEFINED_GROUP_OPTION },
.{ Error.InvalidGroupOption, c.ONIGERR_INVALID_GROUP_OPTION },
.{ Error.InvalidPosixBracketType, c.ONIGERR_INVALID_POSIX_BRACKET_TYPE },
.{ Error.InvalidLookBehindPattern, c.ONIGERR_INVALID_LOOK_BEHIND_PATTERN },
.{ Error.InvalidRepeatRangePattern, c.ONIGERR_INVALID_REPEAT_RANGE_PATTERN },
.{ Error.TooBigNumber, c.ONIGERR_TOO_BIG_NUMBER },
.{ Error.TooBigNumberForRepeatRange, c.ONIGERR_TOO_BIG_NUMBER_FOR_REPEAT_RANGE },
.{ Error.UpperSmallerThanLowerInRepeatRange, c.ONIGERR_UPPER_SMALLER_THAN_LOWER_IN_REPEAT_RANGE },
.{ Error.EmptyRangeInCharClass, c.ONIGERR_EMPTY_RANGE_IN_CHAR_CLASS },
.{ Error.MismatchCodeLengthInClassRange, c.ONIGERR_MISMATCH_CODE_LENGTH_IN_CLASS_RANGE },
.{ Error.TooManyMultiByteRanges, c.ONIGERR_TOO_MANY_MULTI_BYTE_RANGES },
.{ Error.TooShortMultiByteString, c.ONIGERR_TOO_SHORT_MULTI_BYTE_STRING },
.{ Error.TooBigBackrefNumber, c.ONIGERR_TOO_BIG_BACKREF_NUMBER },
.{ Error.InvalidBackref, c.ONIGERR_INVALID_BACKREF },
.{ Error.NumberedBackrefOrCallNotAllowed, c.ONIGERR_NUMBERED_BACKREF_OR_CALL_NOT_ALLOWED },
.{ Error.TooManyCaptures, c.ONIGERR_TOO_MANY_CAPTURES },
.{ Error.TooLongWideCharValue, c.ONIGERR_TOO_LONG_WIDE_CHAR_VALUE },
.{ Error.UndefinedOperator, c.ONIGERR_UNDEFINED_OPERATOR },
.{ Error.EmptyGroupName, c.ONIGERR_EMPTY_GROUP_NAME },
.{ Error.InvalidGroupName, c.ONIGERR_INVALID_GROUP_NAME },
.{ Error.InvalidCharInGroupName, c.ONIGERR_INVALID_CHAR_IN_GROUP_NAME },
.{ Error.UndefinedNameReference, c.ONIGERR_UNDEFINED_NAME_REFERENCE },
.{ Error.UndefinedGroupReference, c.ONIGERR_UNDEFINED_GROUP_REFERENCE },
.{ Error.MultiplexDefinedName, c.ONIGERR_MULTIPLEX_DEFINED_NAME },
.{ Error.MultiplexDefinitionNameCall, c.ONIGERR_MULTIPLEX_DEFINITION_NAME_CALL },
.{ Error.NeverEndingRecursion, c.ONIGERR_NEVER_ENDING_RECURSION },
.{ Error.GroupNumberOverForCaptureHistory, c.ONIGERR_GROUP_NUMBER_OVER_FOR_CAPTURE_HISTORY },
.{ Error.InvalidCharPropertyName, c.ONIGERR_INVALID_CHAR_PROPERTY_NAME },
.{ Error.InvalidIfElseSyntax, c.ONIGERR_INVALID_IF_ELSE_SYNTAX },
.{ Error.InvalidAbsentGroupPattern, c.ONIGERR_INVALID_ABSENT_GROUP_PATTERN },
.{ Error.InvalidAbsentGroupGeneratorPattern, c.ONIGERR_INVALID_ABSENT_GROUP_GENERATOR_PATTERN },
.{ Error.InvalidCalloutPattern, c.ONIGERR_INVALID_CALLOUT_PATTERN },
.{ Error.InvalidCalloutName, c.ONIGERR_INVALID_CALLOUT_NAME },
.{ Error.UndefinedCalloutName, c.ONIGERR_UNDEFINED_CALLOUT_NAME },
.{ Error.InvalidCalloutBody, c.ONIGERR_INVALID_CALLOUT_BODY },
.{ Error.InvalidCalloutTagName, c.ONIGERR_INVALID_CALLOUT_TAG_NAME },
.{ Error.InvalidCalloutArg, c.ONIGERR_INVALID_CALLOUT_ARG },
.{ Error.InvalidCodePointValue, c.ONIGERR_INVALID_CODE_POINT_VALUE },
.{ Error.InvalidWideCharValue, c.ONIGERR_INVALID_WIDE_CHAR_VALUE },
.{ Error.TooBigWideCharValue, c.ONIGERR_TOO_BIG_WIDE_CHAR_VALUE },
.{ Error.NotSupportedEncodingCombination, c.ONIGERR_NOT_SUPPORTED_ENCODING_COMBINATION },
.{ Error.InvalidCombinationOfOptions, c.ONIGERR_INVALID_COMBINATION_OF_OPTIONS },
.{ Error.TooManyUserDefinedObjects, c.ONIGERR_TOO_MANY_USER_DEFINED_OBJECTS },
.{ Error.TooLongPropertyName, c.ONIGERR_TOO_LONG_PROPERTY_NAME },
.{ Error.VeryInefficientPattern, c.ONIGERR_VERY_INEFFICIENT_PATTERN },
.{ Error.LibraryIsNotInitialized, c.ONIGERR_LIBRARY_IS_NOT_INITIALIZED },
};

16
pkg/oniguruma/init.zig Normal file
View File

@ -0,0 +1,16 @@
const c = @import("c.zig");
const Encoding = @import("types.zig").Encoding;
const errors = @import("errors.zig");
/// Call once per process to initialize Oniguruma. This should be given
/// the encodings that the program will use.
pub fn init(encs: []const *Encoding) !void {
_ = try errors.convertError(c.onig_initialize(
@constCast(@ptrCast(@alignCast(encs.ptr))),
@intCast(encs.len),
));
}
pub fn deinit() void {
_ = c.onig_end();
}

11
pkg/oniguruma/main.zig Normal file
View File

@ -0,0 +1,11 @@
pub usingnamespace @import("init.zig");
pub usingnamespace @import("errors.zig");
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());
}

87
pkg/oniguruma/regex.zig Normal file
View File

@ -0,0 +1,87 @@
const std = @import("std");
const c = @import("c.zig");
const types = @import("types.zig");
const errors = @import("errors.zig");
const testEnsureInit = @import("testing.zig").ensureInit;
const Region = @import("region.zig").Region;
const Error = errors.Error;
const ErrorInfo = errors.ErrorInfo;
const Encoding = types.Encoding;
const Option = types.Option;
const Syntax = types.Syntax;
pub const Regex = struct {
value: c.OnigRegex,
pub fn init(
pattern: []const u8,
options: Option,
enc: *Encoding,
syntax: *Syntax,
err: ?*ErrorInfo,
) !Regex {
var self: Regex = undefined;
_ = try errors.convertError(c.onig_new(
&self.value,
pattern.ptr,
pattern.ptr + pattern.len,
options.int(),
@ptrCast(@alignCast(enc)),
@ptrCast(@alignCast(syntax)),
@ptrCast(err),
));
return self;
}
pub fn deinit(self: *Regex) void {
c.onig_free(self.value);
}
/// Search an entire string for matches. This always returns a region
/// which may heap allocate (C allocator).
pub fn search(
self: *Regex,
str: []const u8,
options: Option,
) !Region {
var region: Region = .{};
_ = try self.searchAdvanced(str, 0, str.len, &region, options);
return region;
}
/// onig_search directly
pub fn searchAdvanced(
self: *Regex,
str: []const u8,
start: usize,
end: usize,
region: *Region,
options: Option,
) !usize {
const pos = try errors.convertError(c.onig_search(
self.value,
str.ptr,
str.ptr + str.len,
str.ptr + start,
str.ptr + end,
@ptrCast(region),
options.int(),
));
return @intCast(pos);
}
};
test {
const testing = std.testing;
try testEnsureInit();
var re = try Regex.init("foo", .{}, Encoding.utf8, Syntax.default, null);
defer re.deinit();
var reg = try re.search("hello foo bar", .{});
defer reg.deinit();
try testing.expectEqual(@as(usize, 1), reg.count());
try testing.expectError(Error.Mismatch, re.search("hello", .{}));
}

51
pkg/oniguruma/region.zig Normal file
View File

@ -0,0 +1,51 @@
const std = @import("std");
const c = @import("c.zig");
pub const Region = extern struct {
allocated: c_int = 0,
num_regs: c_int = 0,
beg: ?[*]c_int = null,
end: ?[*]c_int = null,
history_root: ?*c.OnigCaptureTreeNode = null, // TODO: convert to Zig
pub fn deinit(self: *Region) void {
// We never free ourself because allocation of Region in the Zig
// bindings is handled by the Zig program.
c.onig_region_free(@ptrCast(self), 0);
}
/// Count the number of matches
pub fn count(self: *const Region) usize {
return @intCast(self.num_regs);
}
/// Iterate over the matched ranges.
pub fn iterator(self: *const Region) Iterator {
return .{ .region = self };
}
pub fn starts(self: *const Region) []const c_int {
if (self.num_regs == 0) return &.{};
return self.beg.?[0..@intCast(self.num_regs)];
}
pub fn ends(self: *const Region) []const c_int {
if (self.num_regs == 0) return &.{};
return self.end.?[0..@intCast(self.num_regs)];
}
pub const Iterator = struct {
region: *const Region,
i: usize = 0,
/// The next range
pub fn next(self: *Iterator) ?[2]usize {
if (self.i >= self.region.num_regs) return null;
defer self.i += 1;
return .{
@intCast(self.region.beg.?[self.i]),
@intCast(self.region.end.?[self.i]),
};
}
};
};

15
pkg/oniguruma/testing.zig Normal file
View File

@ -0,0 +1,15 @@
const init = @import("init.zig");
const Encoding = @import("types.zig").Encoding;
var initialized: bool = false;
/// Call this function before any other tests in this package to ensure that
/// the oni library is initialized. This should only be used for tests
/// and only when you're sure this is the ONLY way that oni is being
/// initialized.
///
/// This always only initializes the encodings the tests use.
pub fn ensureInit() !void {
if (initialized) return;
try init.init(&.{Encoding.utf8});
}

96
pkg/oniguruma/types.zig Normal file
View File

@ -0,0 +1,96 @@
const std = @import("std");
const c = @import("c.zig");
pub const Encoding = opaque {
pub const ascii: *Encoding = @ptrCast(c.ONIG_ENCODING_ASCII);
pub const iso_8859_1: *Encoding = @ptrCast(c.ONIG_ENCODING_ISO_8859_1);
pub const iso_8859_2: *Encoding = @ptrCast(c.ONIG_ENCODING_ISO_8859_2);
pub const iso_8859_3: *Encoding = @ptrCast(c.ONIG_ENCODING_ISO_8859_3);
pub const iso_8859_4: *Encoding = @ptrCast(c.ONIG_ENCODING_ISO_8859_4);
pub const iso_8859_5: *Encoding = @ptrCast(c.ONIG_ENCODING_ISO_8859_5);
pub const iso_8859_6: *Encoding = @ptrCast(c.ONIG_ENCODING_ISO_8859_6);
pub const iso_8859_7: *Encoding = @ptrCast(c.ONIG_ENCODING_ISO_8859_7);
pub const iso_8859_8: *Encoding = @ptrCast(c.ONIG_ENCODING_ISO_8859_8);
pub const iso_8859_9: *Encoding = @ptrCast(c.ONIG_ENCODING_ISO_8859_9);
pub const iso_8859_10: *Encoding = @ptrCast(c.ONIG_ENCODING_ISO_8859_10);
pub const iso_8859_11: *Encoding = @ptrCast(c.ONIG_ENCODING_ISO_8859_11);
pub const iso_8859_13: *Encoding = @ptrCast(c.ONIG_ENCODING_ISO_8859_13);
pub const iso_8859_14: *Encoding = @ptrCast(c.ONIG_ENCODING_ISO_8859_14);
pub const iso_8859_15: *Encoding = @ptrCast(c.ONIG_ENCODING_ISO_8859_15);
pub const iso_8859_16: *Encoding = @ptrCast(c.ONIG_ENCODING_ISO_8859_16);
pub const utf8: *Encoding = @ptrCast(c.ONIG_ENCODING_UTF8);
pub const utf16_be: *Encoding = @ptrCast(c.ONIG_ENCODING_UTF16_BE);
pub const utf16_le: *Encoding = @ptrCast(c.ONIG_ENCODING_UTF16_LE);
pub const utf32_be: *Encoding = @ptrCast(c.ONIG_ENCODING_UTF32_BE);
pub const utf32_le: *Encoding = @ptrCast(c.ONIG_ENCODING_UTF32_LE);
pub const euc_jp: *Encoding = @ptrCast(c.ONIG_ENCODING_EUC_JP);
pub const euc_tw: *Encoding = @ptrCast(c.ONIG_ENCODING_EUC_TW);
pub const euc_kr: *Encoding = @ptrCast(c.ONIG_ENCODING_EUC_KR);
pub const euc_cn: *Encoding = @ptrCast(c.ONIG_ENCODING_EUC_CN);
pub const sjis: *Encoding = @ptrCast(c.ONIG_ENCODING_SJIS);
pub const koi8: *Encoding = @ptrCast(c.ONIG_ENCODING_KOI8);
pub const koi8_r: *Encoding = @ptrCast(c.ONIG_ENCODING_KOI8_R);
pub const cp1251: *Encoding = @ptrCast(c.ONIG_ENCODING_CP1251);
pub const big5: *Encoding = @ptrCast(c.ONIG_ENCODING_BIG5);
pub const gb18030: *Encoding = @ptrCast(c.ONIG_ENCODING_GB18030);
};
pub const Syntax = opaque {
pub const default: *Syntax = @ptrCast(c.ONIG_SYNTAX_ONIGURUMA);
pub const asis: *Syntax = @ptrCast(c.ONIG_SYNTAX_ASIS);
pub const posix_basic: *Syntax = @ptrCast(c.ONIG_SYNTAX_POSIX_BASIC);
pub const posix_extended: *Syntax = @ptrCast(c.ONIG_SYNTAX_POSIX_EXTENDED);
pub const emacs: *Syntax = @ptrCast(c.ONIG_SYNTAX_EMACS);
pub const grep: *Syntax = @ptrCast(c.ONIG_SYNTAX_GREP);
pub const gnu_regex: *Syntax = @ptrCast(c.ONIG_SYNTAX_GNU_REGEX);
pub const java: *Syntax = @ptrCast(c.ONIG_SYNTAX_JAVA);
pub const perl: *Syntax = @ptrCast(c.ONIG_SYNTAX_PERL);
pub const perl_ng: *Syntax = @ptrCast(c.ONIG_SYNTAX_PERL_NG);
pub const ruby: *Syntax = @ptrCast(c.ONIG_SYNTAX_RUBY);
pub const oniguruma: *Syntax = @ptrCast(c.ONIG_SYNTAX_ONIGURUMA);
};
pub const Option = packed struct(c_uint) {
ignorecase: bool = false,
extend: bool = false,
multiline: bool = false,
singleline: bool = false,
find_longest: bool = false,
find_not_empty: bool = false,
negate_singleline: bool = false,
dont_capture_group: bool = false,
capture_group: bool = false,
// search time
notbol: bool = false,
noteol: bool = false,
posix_region: bool = false,
check_validity_of_string: bool = false,
// compile time
ignorecase_is_ascii: bool = false,
word_is_ascii: bool = false,
digit_is_ascii: bool = false,
space_is_ascii: bool = false,
posix_is_ascii: bool = false,
text_segment_extended_grapheme_cluster: bool = false,
text_segment_word: bool = false,
// search time
not_begin_string: bool = false,
not_end_string: bool = false,
not_begin_position: bool = false,
callback_each_match: bool = false,
match_whole_string: bool = false,
_padding: u7 = 0,
pub fn int(self: Option) c_uint {
return @bitCast(self);
}
test "order" {
const testing = std.testing;
const opt: Option = .{ .extend = true };
try testing.expectEqual(c.ONIG_OPTION_EXTEND, opt.int());
}
};
test {}

View File

@ -20,6 +20,7 @@ const builtin = @import("builtin");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const oni = @import("oniguruma");
const ziglyph = @import("ziglyph");
const main = @import("main.zig");
const renderer = @import("renderer.zig");
@ -137,6 +138,13 @@ const Mouse = struct {
/// True if the mouse is hidden
hidden: bool = false,
/// True if the mouse position is currently over a link.
over_link: bool = false,
/// The last x/y in the cursor position for links. We use this to
/// only process link hover events when the mouse actually moves cells.
link_point: ?terminal.point.Viewport = null,
};
/// The configuration that a surface has, this is copied from the main
@ -165,12 +173,38 @@ const DerivedConfig = struct {
window_padding_y: u32,
window_padding_balance: bool,
title: ?[:0]const u8,
links: []const Link,
const Link = struct {
regex: oni.Regex,
action: input.Link.Action,
};
pub fn init(alloc_gpa: Allocator, config: *const configpkg.Config) !DerivedConfig {
var arena = ArenaAllocator.init(alloc_gpa);
errdefer arena.deinit();
const alloc = arena.allocator();
// Build all of our links
const links = links: {
var links = std.ArrayList(Link).init(alloc);
defer links.deinit();
for (config.link.links.items) |link| {
var regex = try link.oniRegex();
errdefer regex.deinit();
try links.append(.{
.regex = regex,
.action = link.action,
});
}
break :links try links.toOwnedSlice();
};
errdefer {
for (links) |*link| link.regex.deinit();
alloc.free(links);
}
return .{
.original_font_size = config.@"font-size",
.keybind = try config.keybind.clone(alloc),
@ -192,6 +226,7 @@ const DerivedConfig = struct {
.window_padding_y = config.@"window-padding-y",
.window_padding_balance = config.@"window-padding-balance",
.title = config.title,
.links = links,
// Assignments happen sequentially so we have to do this last
// so that the memory is captured from allocs above.
@ -1197,6 +1232,18 @@ pub fn keyCallback(
self.hideMouse();
}
// If our mouse modifiers change, we run a cursor position event.
// This handles the scenario where URL highlighting should be
// toggled for example.
if (!self.mouse.mods.equal(event.mods)) mouse_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 {};
}
// When we are in the middle of a mouse event and we press shift,
// we change the mouse to a text shape so that selection appears
// possible.
@ -1842,6 +1889,18 @@ pub fn mouseButtonCallback(
}
}
// Handle link clicking. We want to do this before we do mouse
// reporting or any other mouse handling because a successfully
// clicked link will swallow the event.
if (button == .left and action == .release and self.mouse.over_link) {
const pos = try self.rt_surface.getCursorPos();
if (self.processLinks(pos)) |processed| {
if (processed) return;
} else |err| {
log.warn("error processing links err={}", .{err});
}
}
// Report mouse events if enabled
{
self.renderer_state.mutex.lock();
@ -1970,6 +2029,65 @@ pub fn mouseButtonCallback(
}
}
/// Returns the link at the given cursor position, if any.
fn linkAtPos(
self: *Surface,
pos: apprt.CursorPos,
) !?struct {
DerivedConfig.Link,
terminal.Selection,
} {
// If we have no configured links we can save a lot of work
if (self.config.links.len == 0) return null;
// Convert our cursor position to a screen point.
const mouse_pt = mouse_pt: {
const viewport_point = self.posToViewport(pos.x, pos.y);
break :mouse_pt viewport_point.toScreen(&self.io.terminal.screen);
};
// Get the line we're hovering over.
const line = self.io.terminal.screen.getLine(mouse_pt) orelse
return null;
const strmap = try line.stringMap(self.alloc);
defer strmap.deinit(self.alloc);
// Go through each link and see if we clicked it
for (self.config.links) |link| {
var it = strmap.searchIterator(link.regex);
while (true) {
var match = (try it.next()) orelse break;
defer match.deinit();
const sel = match.selection();
if (!sel.contains(mouse_pt)) continue;
return .{ link, sel };
}
}
return null;
}
/// Attempt to invoke the action of any link that is under the
/// given position.
///
/// Requires the renderer state mutex is held.
fn processLinks(self: *Surface, pos: apprt.CursorPos) !bool {
const link, const sel = try self.linkAtPos(pos) orelse return false;
switch (link.action) {
.open => {
const str = try self.io.terminal.screen.selectionString(
self.alloc,
sel,
false,
);
defer self.alloc.free(str);
try internal_os.open(self.alloc, str);
},
}
return true;
}
pub fn cursorPosCallback(
self: *Surface,
pos: apprt.CursorPos,
@ -1980,18 +2098,29 @@ pub fn cursorPosCallback(
// Always show the mouse again if it is hidden
if (self.mouse.hidden) self.showMouse();
// The mouse position in the viewport
const pos_vp = self.posToViewport(pos.x, pos.y);
// We always reset the over link status because it will be reprocessed
// below. But we need the old value to know if we need to undo mouse
// shape changes.
const over_link = self.mouse.over_link;
self.mouse.over_link = false;
// We are reading/writing state for the remainder
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
// Update our mouse state. We set this to null initially because we only
// want to set it when we're not selecting or doing any other mouse
// event.
self.renderer_state.mouse.point = null;
// If we have an inspector, we need to always record position information
if (self.inspector) |insp| {
insp.mouse.last_xpos = pos.x;
insp.mouse.last_ypos = pos.y;
const point = self.posToViewport(pos.x, pos.y);
insp.mouse.last_point = point.toScreen(&self.io.terminal.screen);
insp.mouse.last_point = pos_vp.toScreen(&self.io.terminal.screen);
try self.queueRender();
}
@ -1999,7 +2128,6 @@ pub fn cursorPosCallback(
if (self.io.terminal.flags.mouse_event != .none) report: {
// Shift overrides mouse "grabbing" in the window, taken from Kitty.
if (self.mouse.mods.shift and
self.mouse.click_state[@intFromEnum(input.MouseButton.left)] == .press and
!self.mouseShiftCapture(false)) break :report;
// We use the first mouse button we find pressed in order to report
@ -2011,14 +2139,17 @@ pub fn cursorPosCallback(
try self.mouseReport(button, .motion, self.mouse.mods, pos);
// If we were previously over a link, we need to queue a
// render to undo the link state.
if (over_link) try self.queueRender();
// If we're doing mouse motion tracking, we do not support text
// selection.
return;
}
// If the cursor isn't clicked currently, it doesn't matter
if (self.mouse.click_state[@intFromEnum(input.MouseButton.left)] != .press) return;
// Handle cursor position for text selection
if (self.mouse.click_state[@intFromEnum(input.MouseButton.left)] == .press) {
// All roads lead to requiring a re-render at this point.
try self.queueRender();
@ -2037,8 +2168,7 @@ pub fn cursorPosCallback(
}
// Convert to points
const viewport_point = self.posToViewport(pos.x, pos.y);
const screen_point = viewport_point.toScreen(&self.io.terminal.screen);
const screen_point = pos_vp.toScreen(&self.io.terminal.screen);
// Handle dragging depending on click count
switch (self.mouse.left_click_count) {
@ -2047,6 +2177,36 @@ pub fn cursorPosCallback(
3 => self.dragLeftClickTriple(screen_point),
else => unreachable,
}
return;
}
// Handle link hovering
if (self.mouse.link_point) |last_vp| {
// If our last link viewport point is unchanged, then don't process
// links. This avoids constantly reprocessing regular expressions
// for every pixel change.
if (last_vp.eql(pos_vp)) {
// We have to restore old values that are always cleared
if (over_link) {
self.mouse.over_link = over_link;
self.renderer_state.mouse.point = pos_vp;
}
return;
}
}
self.mouse.link_point = pos_vp;
if (try self.linkAtPos(pos)) |_| {
self.renderer_state.mouse.point = pos_vp;
self.mouse.over_link = true;
try self.rt_surface.setMouseShape(.pointer);
try self.queueRender();
} else if (over_link) {
try self.rt_surface.setMouseShape(self.io.terminal.mouse_shape);
try self.queueRender();
}
}
/// Double-click dragging moves the selection one "word" at a time.

View File

@ -912,7 +912,6 @@ fn keyEvent(
ud: ?*anyopaque,
) bool {
const self = userdataSelf(ud.?);
const mods = translateMods(gtk_mods);
const keyval_unicode = c.gdk_keyval_to_unicode(keyval);
const event = c.gtk_event_controller_get_current_event(@ptrCast(ec_key));
@ -986,6 +985,57 @@ fn keyEvent(
if (entry.native == keycode) break :keycode entry.key;
} else .invalid;
// Get our modifiers. We have to translate modifier-only presses here
// to state in the mods manually because GTK only does it AFTER the press
// event.
const mods = mods: {
var mods = translateMods(gtk_mods);
switch (physical_key) {
.left_shift => {
mods.shift = action == .press;
if (mods.shift) mods.sides.shift = .left;
},
.right_shift => {
mods.shift = action == .press;
if (mods.shift) mods.sides.shift = .right;
},
.left_control => {
mods.ctrl = action == .press;
if (mods.ctrl) mods.sides.ctrl = .left;
},
.right_control => {
mods.ctrl = action == .press;
if (mods.ctrl) mods.sides.ctrl = .right;
},
.left_alt => {
mods.alt = action == .press;
if (mods.alt) mods.sides.alt = .left;
},
.right_alt => {
mods.alt = action == .press;
if (mods.alt) mods.sides.alt = .right;
},
.left_super => {
mods.super = action == .press;
if (mods.super) mods.sides.super = .left;
},
.right_super => {
mods.super = action == .press;
if (mods.super) mods.sides.super = .right;
},
else => {},
}
break :mods mods;
};
// Get our consumed modifiers
const consumed_mods: input.Mods = consumed: {
const raw = c.gdk_key_event_get_consumed_modifiers(event);

View File

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

View File

@ -14,6 +14,7 @@ const terminal = @import("../terminal/main.zig");
const internal_os = @import("../os/main.zig");
const cli = @import("../cli.zig");
const url = @import("url.zig");
const Key = @import("key.zig").Key;
const KeyValue = @import("key.zig").Value;
const ErrorList = @import("ErrorList.zig");
@ -329,6 +330,28 @@ command: ?[]const u8 = null,
/// indicate that it is a login shell, depending on the OS).
@"command-arg": RepeatableString = .{},
/// Match a regular expression against the terminal text and associate
/// clicking it with an action. This can be used to match URLs, file paths,
/// etc. Actions can be opening using the system opener (i.e. "open" or
/// "xdg-open") or executing any arbitrary binding action.
///
/// Links that are configured earlier take precedence over links that
/// are configured later.
///
/// A default link that matches a URL and opens it in the system opener
/// always exists. This can be disabled using "link-url".
///
/// 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.
///
/// 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-url": bool = true,
/// Start new windows in fullscreen. This setting applies to new
/// windows and does not apply to tabs, splits, etc. However, this
/// setting will apply to all new windows, not just the first one.
@ -1189,6 +1212,13 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config {
);
}
// Add our default link for URL detection
try result.link.links.append(alloc, .{
.regex = url.regex,
.action = .{ .open = {} },
.highlight = .{ .hover = {} },
});
return result;
}
@ -1527,6 +1557,10 @@ pub fn finalize(self: *Config) !void {
// Minimmum window size
if (self.@"window-width" > 0) self.@"window-width" = @max(10, self.@"window-width");
if (self.@"window-height" > 0) self.@"window-height" = @max(4, self.@"window-height");
// If URLs are disabled, cut off the first link. The first link is
// always the URL matcher.
if (!self.@"link-url") self.link.links.items = self.link.links.items[1..];
}
/// Callback for src/cli/args.zig to allow us to handle special cases
@ -2508,6 +2542,34 @@ pub const FontStyle = union(enum) {
}
};
/// See "link" for documentation.
pub const RepeatableLink = struct {
const Self = @This();
links: std.ArrayListUnmanaged(inputpkg.Link) = .{},
pub fn parseCLI(self: *Self, alloc: Allocator, input_: ?[]const u8) !void {
_ = self;
_ = alloc;
_ = input_;
return error.NotImplemented;
}
/// Deep copy of the struct. Required by Config.
pub fn clone(self: *const Self, alloc: Allocator) !Self {
_ = self;
_ = alloc;
return .{};
}
/// Compare if two of our value are requal. Required by Config.
pub fn equal(self: Self, other: Self) bool {
_ = self;
_ = other;
return true;
}
};
/// Options for copy on select behavior.
pub const CopyOnSelect = enum {
/// Disables copy on select entirely.

26
src/config/url.zig Normal file
View 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();
}
}

View File

@ -7,6 +7,7 @@ pub const function_keys = @import("input/function_keys.zig");
pub const keycodes = @import("input/keycodes.zig");
pub const kitty = @import("input/kitty.zig");
pub const Binding = @import("input/Binding.zig");
pub const Link = @import("input/Link.zig");
pub const KeyEncoder = @import("input/KeyEncoder.zig");
pub const InspectorMode = Binding.Action.InspectorMode;
pub const SplitDirection = Binding.Action.SplitDirection;

44
src/input/Link.zig Normal file
View File

@ -0,0 +1,44 @@
//! A link is a clickable element that can be used to trigger some action.
//! A link is NOT just a URL that opens in a browser. A link is any generic
//! regular expression match over terminal text that can trigger various
//! action types.
const Link = @This();
const oni = @import("oniguruma");
/// 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.
regex: []const u8,
/// The action that will be triggered when the link is clicked.
action: Action,
/// The situations in which the link will be highlighted. A link is only
/// clickable by the mouse when it is highlighted, so this also controls
/// when the link is clickable.
highlight: Highlight,
pub const Action = union(enum) {
/// Open the full matched value using the default open program.
/// For example, on macOS this is "open" and on Linux this is "xdg-open".
open: void,
};
pub const Highlight = union(enum) {
/// Always highlight the link.
always: void,
/// Only highlight the link when the mouse is hovering over it.
hover: void,
};
/// Returns a new oni.Regex that can be used to match the link.
pub fn oniRegex(self: *const Link) !oni.Regex {
return try oni.Regex.init(
self.regex,
.{},
oni.Encoding.utf8,
oni.Syntax.default,
null,
);
}

View File

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

View File

@ -9,6 +9,7 @@ pub usingnamespace @import("homedir.zig");
pub usingnamespace @import("locale.zig");
pub usingnamespace @import("macos_version.zig");
pub usingnamespace @import("mouse.zig");
pub usingnamespace @import("open.zig");
pub usingnamespace @import("pipe.zig");
pub usingnamespace @import("resourcesdir.zig");
pub const TempDir = @import("TempDir.zig");

16
src/os/open.zig Normal file
View File

@ -0,0 +1,16 @@
const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
/// Open a URL in the default handling application.
pub fn open(alloc: Allocator, url: []const u8) !void {
const argv = switch (builtin.os.tag) {
.linux => &.{ "xdg-open", url },
.macos => &.{ "open", url },
.windows => &.{ "rundll32", "url.dll,FileProtocolHandler", url },
else => @compileError("unsupported OS"),
};
var exe = std.process.Child.init(argv, alloc);
try exe.spawn();
}

View File

@ -18,6 +18,7 @@ const terminal = @import("../terminal/main.zig");
const renderer = @import("../renderer.zig");
const math = @import("../math.zig");
const Surface = @import("../Surface.zig");
const link = @import("link.zig");
const shadertoy = @import("shadertoy.zig");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
@ -153,6 +154,7 @@ pub const DerivedConfig = struct {
invert_selection_fg_bg: bool,
custom_shaders: std.ArrayListUnmanaged([]const u8),
custom_shader_animation: bool,
links: link.Set,
pub fn init(
alloc_gpa: Allocator,
@ -174,6 +176,12 @@ pub const DerivedConfig = struct {
font_styles.set(.italic, config.@"font-style-italic" != .false);
font_styles.set(.bold_italic, config.@"font-style-bold-italic" != .false);
// Our link configs
const links = try link.Set.fromConfig(
alloc,
config.link.links.items,
);
return .{
.background_opacity = @max(0, @min(1, config.@"background-opacity")),
.font_thicken = config.@"font-thicken",
@ -208,12 +216,15 @@ pub const DerivedConfig = struct {
.custom_shaders = custom_shaders,
.custom_shader_animation = config.@"custom-shader-animation",
.links = links,
.arena = arena,
};
}
pub fn deinit(self: *DerivedConfig) void {
const alloc = self.arena.allocator();
self.links.deinit(alloc);
self.arena.deinit();
}
};
@ -555,6 +566,7 @@ pub fn updateFrame(
bg: terminal.color.RGB,
selection: ?terminal.Selection,
screen: terminal.Screen,
mouse: renderer.State.Mouse,
preedit: ?renderer.State.Preedit,
cursor_style: ?renderer.CursorStyle,
};
@ -622,6 +634,7 @@ pub fn updateFrame(
.bg = self.background_color,
.selection = selection,
.screen = screen_copy,
.mouse = state.mouse,
.preedit = if (cursor_style != null) state.preedit else null,
.cursor_style = cursor_style,
};
@ -632,6 +645,7 @@ pub fn updateFrame(
try self.rebuildCells(
critical.selection,
&critical.screen,
critical.mouse,
critical.preedit,
critical.cursor_style,
);
@ -1354,6 +1368,7 @@ fn rebuildCells(
self: *Metal,
term_selection: ?terminal.Selection,
screen: *terminal.Screen,
mouse: renderer.State.Mouse,
preedit: ?renderer.State.Preedit,
cursor_style_: ?renderer.CursorStyle,
) !void {
@ -1371,6 +1386,18 @@ 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();
// Create our match set for the links.
var link_match_set = try self.config.links.matchSet(
arena_alloc,
screen,
mouse.point orelse .{},
);
// 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 {
@ -1475,10 +1502,27 @@ 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 our links contain this cell then we want to
// underline it.
if (link_match_set.orderedContains(.{
.x = shaper_cell.x,
.y = y,
})) {
cell.attrs.underline = .single;
}
break :cell cell;
};
if (self.updateCell(
term_selection,
screen,
row.getCell(shaper_cell.x),
cell,
shaper_cell,
run,
shaper_cell.x,

View File

@ -8,6 +8,7 @@ const assert = std.debug.assert;
const testing = std.testing;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const link = @import("link.zig");
const shadertoy = @import("shadertoy.zig");
const apprt = @import("../apprt.zig");
const configpkg = @import("../config.zig");
@ -225,6 +226,7 @@ pub const DerivedConfig = struct {
invert_selection_fg_bg: bool,
custom_shaders: std.ArrayListUnmanaged([]const u8),
custom_shader_animation: bool,
links: link.Set,
pub fn init(
alloc_gpa: Allocator,
@ -246,6 +248,12 @@ pub const DerivedConfig = struct {
font_styles.set(.italic, config.@"font-style-italic" != .false);
font_styles.set(.bold_italic, config.@"font-style-bold-italic" != .false);
// Our link configs
const links = try link.Set.fromConfig(
alloc,
config.link.links.items,
);
return .{
.background_opacity = @max(0, @min(1, config.@"background-opacity")),
.font_thicken = config.@"font-thicken",
@ -280,12 +288,15 @@ pub const DerivedConfig = struct {
.custom_shaders = custom_shaders,
.custom_shader_animation = config.@"custom-shader-animation",
.links = links,
.arena = arena,
};
}
pub fn deinit(self: *DerivedConfig) void {
const alloc = self.arena.allocator();
self.links.deinit(alloc);
self.arena.deinit();
}
};
@ -598,6 +609,7 @@ pub fn updateFrame(
gl_bg: terminal.color.RGB,
selection: ?terminal.Selection,
screen: terminal.Screen,
mouse: renderer.State.Mouse,
preedit: ?renderer.State.Preedit,
cursor_style: ?renderer.CursorStyle,
};
@ -665,6 +677,7 @@ pub fn updateFrame(
.gl_bg = self.background_color,
.selection = selection,
.screen = screen_copy,
.mouse = state.mouse,
.preedit = if (cursor_style != null) state.preedit else null,
.cursor_style = cursor_style,
};
@ -683,6 +696,7 @@ pub fn updateFrame(
try self.rebuildCells(
critical.selection,
&critical.screen,
critical.mouse,
critical.preedit,
critical.cursor_style,
);
@ -855,6 +869,7 @@ pub fn rebuildCells(
self: *OpenGL,
term_selection: ?terminal.Selection,
screen: *terminal.Screen,
mouse: renderer.State.Mouse,
preedit: ?renderer.State.Preedit,
cursor_style_: ?renderer.CursorStyle,
) !void {
@ -877,9 +892,21 @@ pub 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();
// We've written no data to the GPU, refresh it all
self.gl_cells_written = 0;
// Create our match set for the links.
var link_match_set = try self.config.links.matchSet(
arena_alloc,
screen,
mouse.point orelse .{},
);
// 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 {
@ -975,10 +1002,27 @@ pub 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 our links contain this cell then we want to
// underline it.
if (link_match_set.orderedContains(.{
.x = shaper_cell.x,
.y = y,
})) {
cell.attrs.underline = .single;
}
break :cell cell;
};
if (self.updateCell(
term_selection,
screen,
row.getCell(shaper_cell.x),
cell,
shaper_cell,
run,
shaper_cell.x,

View File

@ -25,6 +25,17 @@ inspector: ?*Inspector = null,
/// a future exercise.
preedit: ?Preedit = null,
/// Mouse state. This only contains state relevant to what renderers
/// need about the mouse.
mouse: Mouse = .{},
pub const Mouse = struct {
/// The point on the viewport where the mouse currently is. We use
/// viewport points to avoid the complexity of mapping the mouse to
/// the renderer state.
point: ?terminal.point.Viewport = null,
};
/// The pre-edit state. See Surface.preeditCallback for more information.
pub const Preedit = struct {
/// The codepoints to render as preedit text. We allow up to 16 codepoints

257
src/renderer/link.zig Normal file
View File

@ -0,0 +1,257 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const oni = @import("oniguruma");
const configpkg = @import("../config.zig");
const inputpkg = @import("../input.zig");
const terminal = @import("../terminal/main.zig");
const point = terminal.point;
const Screen = terminal.Screen;
const log = std.log.scoped(.renderer_link);
/// The link configuration needed for renderers.
pub const Link = struct {
/// The regular expression to match the link against.
regex: oni.Regex,
/// The situations in which the link should be highlighted.
highlight: inputpkg.Link.Highlight,
pub fn deinit(self: *Link) void {
self.regex.deinit();
}
};
/// A set of links. This provides a higher level API for renderers
/// to match against a viewport and determine if cells are part of
/// a link.
pub const Set = struct {
links: []Link,
/// Returns the slice of links from the configuration.
pub fn fromConfig(
alloc: Allocator,
config: []const inputpkg.Link,
) !Set {
var links = std.ArrayList(Link).init(alloc);
defer links.deinit();
for (config) |link| {
var regex = try link.oniRegex();
errdefer regex.deinit();
try links.append(.{
.regex = regex,
.highlight = link.highlight,
});
}
return .{ .links = try links.toOwnedSlice() };
}
pub fn deinit(self: *Set, alloc: Allocator) void {
for (self.links) |*link| link.deinit();
alloc.free(self.links);
}
/// Returns the matchset for the viewport state. The matchset is the
/// full set of matching links for the visible viewport. A link
/// only matches if it is also in the correct state (i.e. hovered
/// if necessary).
///
/// This is not a particularly efficient operation. This should be
/// called sparingly.
pub fn matchSet(
self: *const Set,
alloc: Allocator,
screen: *Screen,
mouse_vp_pt: point.Viewport,
) !MatchSet {
// Convert the viewport point to a screen point.
const mouse_pt = mouse_vp_pt.toScreen(screen);
// This contains our list of matches. The matches are stored
// as selections which contain the start and end points of
// the match. There is no way to map these back to the link
// configuration right now because we don't need to.
var matches = std.ArrayList(terminal.Selection).init(alloc);
defer matches.deinit();
// Iterate over all the visible lines.
var lineIter = screen.lineIterator(.viewport);
while (lineIter.next()) |line| {
const strmap = line.stringMap(alloc) catch |err| {
log.warn(
"failed to build string map for link checking err={}",
.{err},
);
continue;
};
defer strmap.deinit(alloc);
// 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;
}
var it = strmap.searchIterator(link.regex);
while (true) {
const match_ = it.next() catch |err| {
log.warn("failed to search for link err={}", .{err});
break;
};
var match = match_ orelse break;
defer match.deinit();
const sel = match.selection();
// If this is a highlight link then we only want to
// include matches that include our hover point.
if (link.highlight == .hover and
!sel.contains(mouse_pt))
{
continue;
}
try matches.append(sel);
}
}
}
return .{ .matches = try matches.toOwnedSlice() };
}
};
/// MatchSet is the result of matching links against a screen. This contains
/// all the matching links and operations on them such as whether a specific
/// cell is part of a matched link.
pub const MatchSet = struct {
/// The matches.
///
/// Important: this must be in left-to-right top-to-bottom order.
matches: []const terminal.Selection,
i: usize = 0,
pub fn deinit(self: *MatchSet, alloc: Allocator) void {
alloc.free(self.matches);
}
/// Checks if the matchset contains the given pt. The points must be
/// given in left-to-right top-to-bottom order. This is a stateful
/// operation and giving a point out of order can cause invalid
/// results.
pub fn orderedContains(
self: *MatchSet,
pt: point.ScreenPoint,
) bool {
// If we're beyond the end of our possible matches, we're done.
if (self.i >= self.matches.len) return false;
// If our selection ends before the point, then no point will ever
// again match this selection so we move on to the next one.
while (self.matches[self.i].end.before(pt)) {
self.i += 1;
if (self.i >= self.matches.len) return false;
}
return self.matches[self.i].contains(pt);
}
};
test "matchset" {
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 = {} },
},
});
defer set.deinit(alloc);
// Get our matches
var match = try set.matchSet(alloc, &s, .{});
defer match.deinit(alloc);
try testing.expectEqual(@as(usize, 2), 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 }));
}
test "matchset hover links" {
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 = .{ .hover = {} },
},
.{
.regex = "EF",
.action = .{ .open = {} },
.highlight = .{ .always = {} },
},
});
defer set.deinit(alloc);
// Not hovering over the first link
{
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 }));
}
// Hovering over the first link
{
var match = try set.matchSet(alloc, &s, .{ .x = 1, .y = 0 });
defer match.deinit(alloc);
try testing.expectEqual(@as(usize, 2), 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 }));
}
}

View File

@ -13,6 +13,9 @@
//! affect this area.
//! * Viewport - The area that is currently visible to the user. This
//! can be thought of as the current window into the screen.
//! * Row - A single visible row in the screen.
//! * Line - A single line of text. This may map to multiple rows if
//! the row is soft-wrapped.
//!
//! The internal storage of the screen is stored in a circular buffer
//! with roughly the following format:
@ -64,6 +67,7 @@ const kitty = @import("kitty.zig");
const point = @import("point.zig");
const CircBuf = @import("../circ_buf.zig").CircBuf;
const Selection = @import("Selection.zig");
const StringMap = @import("StringMap.zig");
const fastmem = @import("../fastmem.zig");
const charsets = @import("charsets.zig");
@ -900,6 +904,72 @@ pub const GraphemeData = union(enum) {
}
};
/// A line represents a line of text, potentially across soft-wrapped
/// boundaries. This differs from row, which is a single physical row within
/// the terminal screen.
pub const Line = struct {
screen: *Screen,
tag: RowIndexTag,
start: usize,
len: usize,
/// Return the string for this line.
pub fn string(self: *const Line, alloc: Allocator) ![:0]const u8 {
return try self.screen.selectionString(alloc, self.selection(), true);
}
/// Receive the string for this line along with the byte-to-point mapping.
pub fn stringMap(self: *const Line, alloc: Allocator) !StringMap {
return try self.screen.selectionStringMap(alloc, self.selection());
}
/// Return a selection that covers the entire line.
pub fn selection(self: *const Line) Selection {
// Get the start and end screen point.
const start_idx = self.tag.index(self.start).toScreen(self.screen).screen;
const end_idx = self.tag.index(self.start + (self.len - 1)).toScreen(self.screen).screen;
// Convert the start and end screen points into a selection across
// the entire rows. We then use selectionString because it handles
// unwrapping, graphemes, etc.
return .{
.start = .{ .y = start_idx, .x = 0 },
.end = .{ .y = end_idx, .x = self.screen.cols - 1 },
};
}
};
/// Iterator over textual lines within the terminal. This will unwrap
/// wrapped lines and consider them a single line.
pub const LineIterator = struct {
row_it: RowIterator,
pub fn next(self: *LineIterator) ?Line {
const start = self.row_it.value;
// Get our current row
var row = self.row_it.next() orelse return null;
var len: usize = 1;
// While the row is wrapped we keep iterating over the rows
// and incrementing the length.
while (row.isWrapped()) {
// Note: this orelse shouldn't happen. A wrapped row should
// always have a next row. However, this isn't the place where
// we want to assert that.
row = self.row_it.next() orelse break;
len += 1;
}
return .{
.screen = self.row_it.screen,
.tag = self.row_it.tag,
.start = start,
.len = len,
};
}
};
// Initialize to header and not a cell so that we can check header.init
// to know if the remainder of the row has been initialized or not.
const StorageBuf = CircBuf(StorageCell, .{ .header = .{} });
@ -1097,6 +1167,50 @@ pub fn rowIterator(self: *Screen, tag: RowIndexTag) RowIterator {
};
}
/// Returns an iterator that iterates over the lines of the screen. A line
/// is a single line of text which may wrap across multiple rows. A row
/// is a single physical row of the terminal.
pub fn lineIterator(self: *Screen, tag: RowIndexTag) LineIterator {
return .{ .row_it = self.rowIterator(tag) };
}
/// Returns the line that contains the given point. This may be null if the
/// point is outside the screen.
pub fn getLine(self: *Screen, pt: point.ScreenPoint) ?Line {
// If our y is outside of our written area, we have no line.
if (pt.y >= RowIndexTag.screen.maxLen(self)) return null;
if (pt.x >= self.cols) return null;
// Find the starting y. We go back and as soon as we find a row that
// isn't wrapped, we know the NEXT line is the one we want.
const start_y: usize = if (pt.y == 0) 0 else start_y: {
for (1..pt.y) |y| {
const bot_y = pt.y - y;
const row = self.getRow(.{ .screen = bot_y });
if (!row.isWrapped()) break :start_y bot_y + 1;
}
break :start_y 0;
};
// Find the end y, which is the first row that isn't wrapped.
const end_y = end_y: {
for (pt.y..self.rowsWritten()) |y| {
const row = self.getRow(.{ .screen = y });
if (!row.isWrapped()) break :end_y y;
}
break :end_y self.rowsWritten() - 1;
};
return .{
.screen = self,
.tag = .screen,
.start = start_y,
.len = (end_y - start_y) + 1,
};
}
/// Returns the row at the given index. This row is writable, although
/// only the active area should probably be written to.
pub fn getRow(self: *Screen, index: RowIndex) Row {
@ -2076,62 +2190,83 @@ pub fn selectionString(
// Get the slices for the string
const slices = self.selectionSlices(sel);
// We can now know how much space we'll need to store the string. We loop
// over and UTF8-encode and calculate the exact size required. We will be
// off here by at most "newlines" values in the worst case that every
// single line is soft-wrapped.
const chars = chars: {
var count: usize = 0;
// Use an ArrayList so that we can grow the array as we go. We
// build an initial capacity of just our rows in our selection times
// columns. It can be more or less based on graphemes, newlines, etc.
var strbuilder = try std.ArrayList(u8).initCapacity(alloc, slices.rows * self.cols);
defer strbuilder.deinit();
// We need to keep track of our x/y so that we can get graphemes.
var y: usize = slices.sel.start.y;
var x: usize = 0;
var row: Row = undefined;
// Get our string result.
try self.selectionSliceString(slices, &strbuilder, null);
const arr = [_][]StorageCell{ slices.top, slices.bot };
for (arr) |slice| {
for (slice, 0..) |cell, i| {
// detect row headers
if (@mod(i, self.cols + 1) == 0) {
// We use each row header as an opportunity to "count"
// a new row, and therefore count a possible newline.
count += 1;
// Remove any trailing spaces on lines. We could do optimize this by
// doing this in the loop above but this isn't very hot path code and
// this is simple.
if (trim) {
var it = std.mem.tokenize(u8, strbuilder.items, "\n");
// Increase our row count and get our next row
y += 1;
x = 0;
row = self.getRow(.{ .screen = y - 1 });
continue;
// Reset our items. We retain our capacity. Because we're only
// removing bytes, we know that the trimmed string must be no longer
// than the original string so we copy directly back into our
// allocated memory.
strbuilder.clearRetainingCapacity();
while (it.next()) |line| {
const trimmed = std.mem.trimRight(u8, line, " \t");
const i = strbuilder.items.len;
strbuilder.items.len += trimmed.len;
std.mem.copyForwards(u8, strbuilder.items[i..], trimmed);
strbuilder.appendAssumeCapacity('\n');
}
var buf: [4]u8 = undefined;
const char = if (cell.cell.char > 0) cell.cell.char else ' ';
count += try std.unicode.utf8Encode(@intCast(char), &buf);
// We need to also count any grapheme chars
var it = row.codepointIterator(x);
while (it.next()) |cp| {
count += try std.unicode.utf8Encode(cp, &buf);
// Remove our trailing newline again
if (strbuilder.items.len > 0) strbuilder.items.len -= 1;
}
x += 1;
}
}
// Get our final string
const string = try strbuilder.toOwnedSliceSentinel(0);
errdefer alloc.free(string);
break :chars count;
};
const buf = try alloc.alloc(u8, chars + 1);
errdefer alloc.free(buf);
return string;
}
// Special case the empty case
if (chars == 0) {
buf[0] = 0;
return buf[0..0 :0];
}
/// Returns the row text associated with a selection along with the
/// mapping of each individual byte in the string to the point in the screen.
fn selectionStringMap(
self: *Screen,
alloc: Allocator,
sel: Selection,
) !StringMap {
// Get the slices for the string
const slices = self.selectionSlices(sel);
// Use an ArrayList so that we can grow the array as we go. We
// build an initial capacity of just our rows in our selection times
// columns. It can be more or less based on graphemes, newlines, etc.
var strbuilder = try std.ArrayList(u8).initCapacity(alloc, slices.rows * self.cols);
defer strbuilder.deinit();
var mapbuilder = try std.ArrayList(point.ScreenPoint).initCapacity(alloc, strbuilder.capacity);
defer mapbuilder.deinit();
// Get our results
try self.selectionSliceString(slices, &strbuilder, &mapbuilder);
// Get our final string
const string = try strbuilder.toOwnedSliceSentinel(0);
errdefer alloc.free(string);
const map = try mapbuilder.toOwnedSlice();
errdefer alloc.free(map);
return .{ .string = string, .map = map };
}
/// Takes a SelectionSlices value and builds the string and mapping for it.
fn selectionSliceString(
self: *Screen,
slices: SelectionSlices,
strbuilder: *std.ArrayList(u8),
mapbuilder: ?*std.ArrayList(point.ScreenPoint),
) !void {
// Connect the text from the two slices
const arr = [_][]StorageCell{ slices.top, slices.bot };
var buf_i: usize = 0;
var row_count: usize = 0;
for (arr) |slice| {
const row_start: usize = row_count;
@ -2151,6 +2286,13 @@ pub fn selectionString(
// the first row.
var skip: usize = if (row_count == 0) slices.top_offset else 0;
// If we have runtime safety we need to initialize the row
// so that the proper union tag is set. In release modes we
// don't need to do this because we zero the memory.
if (std.debug.runtime_safety) {
_ = self.getRow(.{ .screen = slices.sel.start.y + row_i });
}
const row: Row = .{ .screen = self, .storage = slice[start_idx..end_idx] };
var it = row.cellIterator();
var x: usize = 0;
@ -2166,56 +2308,61 @@ pub fn selectionString(
if (cell.attrs.wide_spacer_head or
cell.attrs.wide_spacer_tail) continue;
var buf: [4]u8 = undefined;
const char = if (cell.char > 0) cell.char else ' ';
buf_i += try std.unicode.utf8Encode(@intCast(char), buf[buf_i..]);
{
const encode_len = try std.unicode.utf8Encode(@intCast(char), &buf);
try strbuilder.appendSlice(buf[0..encode_len]);
if (mapbuilder) |b| {
for (0..encode_len) |_| try b.append(.{
.x = x,
.y = slices.sel.start.y + row_i,
});
}
}
var cp_it = row.codepointIterator(x);
while (cp_it.next()) |cp| {
buf_i += try std.unicode.utf8Encode(cp, buf[buf_i..]);
const encode_len = try std.unicode.utf8Encode(cp, &buf);
try strbuilder.appendSlice(buf[0..encode_len]);
if (mapbuilder) |b| {
for (0..encode_len) |_| try b.append(.{
.x = x,
.y = slices.sel.start.y + row_i,
});
}
}
}
// If this row is not soft-wrapped, add a newline
if (!row.header().flags.wrap) {
buf[buf_i] = '\n';
buf_i += 1;
try strbuilder.append('\n');
if (mapbuilder) |b| {
try b.append(.{
.x = self.cols - 1,
.y = slices.sel.start.y + row_i,
});
}
}
}
}
// Remove our trailing newline, its never correct.
if (buf_i > 0 and buf[buf_i - 1] == '\n') buf_i -= 1;
// Remove any trailing spaces on lines. We could do optimize this by
// doing this in the loop above but this isn't very hot path code and
// this is simple.
if (trim) {
var it = std.mem.tokenize(u8, buf[0..buf_i], "\n");
buf_i = 0;
while (it.next()) |line| {
const trimmed = std.mem.trimRight(u8, line, " \t");
std.mem.copy(u8, buf[buf_i..], trimmed);
buf_i += trimmed.len;
buf[buf_i] = '\n';
buf_i += 1;
if (strbuilder.items.len > 0 and
strbuilder.items[strbuilder.items.len - 1] == '\n')
{
strbuilder.items.len -= 1;
if (mapbuilder) |b| b.items.len -= 1;
}
// Remove our trailing newline again
if (buf_i > 0) buf_i -= 1;
if (std.debug.runtime_safety) {
if (mapbuilder) |b| {
assert(strbuilder.items.len == b.items.len);
}
}
// Add null termination
buf[buf_i] = 0;
// Realloc so our free length is exactly correct
const result = try alloc.realloc(buf, buf_i + 1);
return result[0..buf_i :0];
}
/// Returns the slices that make up the selection, in order. There are at most
/// two parts to handle the ring buffer. If the selection fits in one contiguous
/// slice, then the second slice will have a length of zero.
fn selectionSlices(self: *Screen, sel_raw: Selection) struct {
const SelectionSlices = struct {
rows: usize,
// The selection that the slices below represent. This may not
@ -2228,7 +2375,12 @@ fn selectionSlices(self: *Screen, sel_raw: Selection) struct {
top_offset: usize,
top: []StorageCell,
bot: []StorageCell,
} {
};
/// Returns the slices that make up the selection, in order. There are at most
/// two parts to handle the ring buffer. If the selection fits in one contiguous
/// slice, then the second slice will have a length of zero.
fn selectionSlices(self: *Screen, sel_raw: Selection) SelectionSlices {
// Note: this function is tested via selectionString
// If the selection starts beyond the end of the screen, then we return empty
@ -3404,6 +3556,91 @@ test "Screen: write long emoji" {
try testing.expectEqual(@as(usize, 5), s.cursor.x);
}
test "Screen: lineIterator" {
const testing = std.testing;
const alloc = testing.allocator;
var s = try init(alloc, 5, 5, 0);
defer s.deinit();
// Sanity check that our test helpers work
const str = "1ABCD\n2EFGH";
try s.testWriteString(str);
// Test the line iterator
var iter = s.lineIterator(.viewport);
{
const line = iter.next().?;
const actual = try line.string(alloc);
defer alloc.free(actual);
try testing.expectEqualStrings("1ABCD", actual);
}
{
const line = iter.next().?;
const actual = try line.string(alloc);
defer alloc.free(actual);
try testing.expectEqualStrings("2EFGH", actual);
}
try testing.expect(iter.next() == null);
}
test "Screen: lineIterator soft wrap" {
const testing = std.testing;
const alloc = testing.allocator;
var s = try init(alloc, 5, 5, 0);
defer s.deinit();
// Sanity check that our test helpers work
const str = "1ABCD2EFGH\n3ABCD";
try s.testWriteString(str);
// Test the line iterator
var iter = s.lineIterator(.viewport);
{
const line = iter.next().?;
const actual = try line.string(alloc);
defer alloc.free(actual);
try testing.expectEqualStrings("1ABCD2EFGH", actual);
}
{
const line = iter.next().?;
const actual = try line.string(alloc);
defer alloc.free(actual);
try testing.expectEqualStrings("3ABCD", actual);
}
try testing.expect(iter.next() == null);
}
test "Screen: getLine soft wrap" {
const testing = std.testing;
const alloc = testing.allocator;
var s = try init(alloc, 5, 5, 0);
defer s.deinit();
// Sanity check that our test helpers work
const str = "1ABCD2EFGH\n3ABCD";
try s.testWriteString(str);
// Test the line iterator
{
const line = s.getLine(.{ .x = 2, .y = 1 }).?;
const actual = try line.string(alloc);
defer alloc.free(actual);
try testing.expectEqualStrings("1ABCD2EFGH", actual);
}
{
const line = s.getLine(.{ .x = 2, .y = 2 }).?;
const actual = try line.string(alloc);
defer alloc.free(actual);
try testing.expectEqualStrings("3ABCD", actual);
}
try testing.expect(s.getLine(.{ .x = 2, .y = 3 }) == null);
try testing.expect(s.getLine(.{ .x = 7, .y = 1 }) == null);
}
test "Screen: scrolling" {
const testing = std.testing;
const alloc = testing.allocator;

124
src/terminal/StringMap.zig Normal file
View File

@ -0,0 +1,124 @@
/// A string along with the mapping of each individual byte in the string
/// to the point in the screen.
const StringMap = @This();
const std = @import("std");
const oni = @import("oniguruma");
const point = @import("point.zig");
const Selection = @import("Selection.zig");
const Screen = @import("Screen.zig");
const Allocator = std.mem.Allocator;
string: [:0]const u8,
map: []point.ScreenPoint,
pub fn deinit(self: StringMap, alloc: Allocator) void {
alloc.free(self.string);
alloc.free(self.map);
}
/// Returns an iterator that yields the next match of the given regex.
pub fn searchIterator(
self: StringMap,
regex: oni.Regex,
) SearchIterator {
return .{ .map = self, .regex = regex };
}
/// Iterates over the regular expression matches of the string.
pub const SearchIterator = struct {
map: StringMap,
regex: oni.Regex,
offset: usize = 0,
/// Returns the next regular expression match or null if there are
/// no more matches.
pub fn next(self: *SearchIterator) !?Match {
if (self.offset >= self.map.string.len) return null;
var region = self.regex.search(
self.map.string[self.offset..],
.{},
) catch |err| switch (err) {
error.Mismatch => {
self.offset = self.map.string.len;
return null;
},
else => return err,
};
errdefer region.deinit();
// Increment our offset by the number of bytes in the match.
// We defer this so that we can return the match before
// modifying the offset.
const end_idx: usize = @intCast(region.ends()[0]);
defer self.offset += end_idx;
return .{
.map = self.map,
.offset = self.offset,
.region = region,
};
}
};
/// A single regular expression match.
pub const Match = struct {
map: StringMap,
offset: usize,
region: oni.Region,
pub fn deinit(self: *Match) void {
self.region.deinit();
}
/// Returns the selection containing the full match.
pub fn selection(self: Match) Selection {
const start_idx: usize = @intCast(self.region.starts()[0]);
const end_idx: usize = @intCast(self.region.ends()[0] - 1);
const start_pt = self.map.map[self.offset + start_idx];
const end_pt = self.map.map[self.offset + end_idx];
return .{ .start = start_pt, .end = end_pt };
}
};
test "searchIterator" {
const testing = std.testing;
const alloc = testing.allocator;
// Initialize our regex
try oni.testing.ensureInit();
var re = try oni.Regex.init(
"[A-B]{2}",
.{},
oni.Encoding.utf8,
oni.Syntax.default,
null,
);
defer re.deinit();
// Initialize our screen
var s = try Screen.init(alloc, 5, 5, 0);
defer s.deinit();
const str = "1ABCD2EFGH\n3IJKL";
try s.testWriteString(str);
const line = s.getLine(.{ .x = 2, .y = 1 }).?;
const map = try line.stringMap(alloc);
defer map.deinit(alloc);
// Get our iterator
var it = map.searchIterator(re);
{
var match = (try it.next()).?;
defer match.deinit();
const sel = match.selection();
try testing.expectEqual(Selection{
.start = .{ .x = 1, .y = 0 },
.end = .{ .x = 2, .y = 0 },
}, sel);
}
try testing.expect(try it.next() == null);
}

View File

@ -26,6 +26,7 @@ pub const Terminal = @import("Terminal.zig");
pub const Parser = @import("Parser.zig");
pub const Selection = @import("Selection.zig");
pub const Screen = @import("Screen.zig");
pub const StringMap = @import("StringMap.zig");
pub const Stream = stream.Stream;
pub const Cursor = Screen.Cursor;
pub const CursorStyleReq = ansi.CursorStyle;