feat(config): port the configuration system and CLI argument parser
Ports ZIG/src/config (minus C/WASM APIs) and cli/{args,CommaSplitter,
diagnostics}.zig into ghostty-config:
- all 202 options with exact names, types, defaults and verbatim docs,
declared through one config_struct! macro; Zig's comptime reflection is
replaced by the ConfigField/CliTarget traits
- loading: default XDG files, -e, config-file includes (optional '?',
cycle detection), themes incl. light:/dark: pairs, conditional config,
replay, finalize() derived defaults, clone/changed diffing for reload
- Linux/GTK default keybinds built on ghostty_input::binding::Set
- formatter producing +show-config output
Verified against the installed Ghostty 1.3.1: `+show-config --default`
and `--default --docs` match byte for byte (except gtk-opengl-debug,
which differs between debug/release builds), as do two complex loaded
configs.
Open items (docs/rewrite/notes/ghostty-config.md): url regex needs an
Oniguruma-compatible engine (onig) in the renderer; some font/terminal
types are local copies awaiting integration; KeyRemapSet equality is
order sensitive unlike Zig.
Test Plan:
- cargo test -p ghostty-config (255 tests)
- compare `ghostty +show-config --default --docs` with our formatter
This commit is contained in:
1 parent
47b479dcfb
commit
67367dcaf5
34 files changed
+14894
-1
No files matched your search
@@ -0,0 +1,362 @@
|
||||
//! Default URL/path regex (port of `config/url.zig`).
|
||||
//!
|
||||
//! The pattern uses Oniguruma (Ruby syntax) features such as variable-length
|
||||
//! lookbehind; use the `onig` crate to compile it.
|
||||
|
||||
use const_format::concatcp;
|
||||
|
||||
const URL_SCHEMES: &str = r"https?://|mailto:|ftp://|file:|ssh:|git://|ssh://|tel:|magnet:|ipfs://|ipns://|gemini://|gopher://|news:";
|
||||
|
||||
const IPV6_URL_PATTERN: &str = r"(?:\[[:0-9a-fA-F]+(?:[:0-9a-fA-F]*)+\](?::[0-9]+)?)";
|
||||
|
||||
const SCHEME_URL_CHARS: &str = r"[\w\-.~:/?#@!$&*+,;=%]";
|
||||
|
||||
const PATH_CHARS: &str = r"[\w\-.~:\/?#@!$&*+;=%]";
|
||||
|
||||
const OPTIONAL_BRACKETED_WORD_SUFFIX: &str = r"(?:[\(\[]\w*[\)\]])?";
|
||||
|
||||
const NO_TRAILING_PUNCTUATION: &str = r"(?<![,.])";
|
||||
|
||||
const NO_TRAILING_COLON: &str = r"(?<!:)";
|
||||
|
||||
const TRAILING_SPACES_AT_EOL: &str = r"(?: +(?= *$))?";
|
||||
|
||||
const DOTTED_PATH_LOOKAHEAD: &str = r"(?=[\w\-.~:\/?#@!$&*+;=%]*\.)";
|
||||
|
||||
const NON_DOTTED_PATH_LOOKAHEAD: &str = r"(?![\w\-.~:\/?#@!$&*+;=%]*\.)";
|
||||
|
||||
const DOTTED_PATH_SPACE_SEGMENTS: &str =
|
||||
r"(?:(?<!:) (?!\w+:\/\/)(?!\.{0,2}\/)(?!~\/)[\w\-.~:\/?#@!$&*+;=%]*[\/.])*";
|
||||
|
||||
const ANY_PATH_SPACE_SEGMENTS: &str =
|
||||
r"(?:(?<!:) (?!\w+:\/\/)(?!\.{0,2}\/)(?!~\/)[\w\-.~:\/?#@!$&*+;=%]+)*";
|
||||
|
||||
const ROOTED_OR_RELATIVE_PATH_PREFIX: &str = r"(?:\.\.\/|\.\/|(?<!\w)~\/|(?:[\w][\w\-.]*\/)*(?<!\w)\$[A-Za-z_]\w*\/|\.[\w][\w\-.]*\/|(?<![\w~\/])\/(?!\/))";
|
||||
|
||||
const BARE_RELATIVE_PATH_PREFIX: &str = r"(?<!\$\d*)(?<!\w)[\w][\w\-.]*\/";
|
||||
|
||||
// Branch 1: URLs with explicit schemes (http, mailto, ftp, etc.).
|
||||
const SCHEME_URL_BRANCH: &str = concatcp!(
|
||||
"(?:",
|
||||
URL_SCHEMES,
|
||||
")",
|
||||
"(?:",
|
||||
IPV6_URL_PATTERN,
|
||||
"|",
|
||||
SCHEME_URL_CHARS,
|
||||
"+",
|
||||
OPTIONAL_BRACKETED_WORD_SUFFIX,
|
||||
")+",
|
||||
NO_TRAILING_PUNCTUATION
|
||||
);
|
||||
|
||||
// Branch 2: Absolute paths and dot-relative paths (/, ./, ../).
|
||||
// A dotted segment is treated as file-like, while the undotted case stays
|
||||
// broad to capture directory-like paths with spaces.
|
||||
const ROOTED_OR_RELATIVE_PATH_BRANCH: &str = concatcp!(
|
||||
ROOTED_OR_RELATIVE_PATH_PREFIX,
|
||||
"(?:",
|
||||
DOTTED_PATH_LOOKAHEAD,
|
||||
PATH_CHARS,
|
||||
"+",
|
||||
DOTTED_PATH_SPACE_SEGMENTS,
|
||||
NO_TRAILING_COLON,
|
||||
TRAILING_SPACES_AT_EOL,
|
||||
"|",
|
||||
NON_DOTTED_PATH_LOOKAHEAD,
|
||||
PATH_CHARS,
|
||||
"+",
|
||||
ANY_PATH_SPACE_SEGMENTS,
|
||||
NO_TRAILING_COLON,
|
||||
TRAILING_SPACES_AT_EOL,
|
||||
")"
|
||||
);
|
||||
|
||||
// Branch 3: Bare relative paths such as src/config/url.zig.
|
||||
const BARE_RELATIVE_PATH_BRANCH: &str = concatcp!(
|
||||
DOTTED_PATH_LOOKAHEAD,
|
||||
BARE_RELATIVE_PATH_PREFIX,
|
||||
PATH_CHARS,
|
||||
"+",
|
||||
NO_TRAILING_COLON,
|
||||
TRAILING_SPACES_AT_EOL
|
||||
);
|
||||
|
||||
/// Default URL/path regex. This is used to detect URLs and file paths in
|
||||
/// terminal output.
|
||||
///
|
||||
/// This is here in the config package because one day the matchers will be
|
||||
/// configurable and this will be a default.
|
||||
///
|
||||
/// For scheme URLs, this regex is liberal in what it accepts after the scheme,
|
||||
/// with exceptions for URLs ending with . or ). Although such URLs are
|
||||
/// perfectly valid, it is common for text to contain URLs surrounded by
|
||||
/// parentheses (such as in Markdown links) or at the end of sentences.
|
||||
/// Therefore, this regex excludes them as follows:
|
||||
///
|
||||
/// 1. Do not match regexes ending with .
|
||||
/// 2. Do not match regexes ending with ), except for ones which contain a (
|
||||
/// without a subsequent )
|
||||
///
|
||||
/// Rule 2 means that that we handle the following two cases:
|
||||
///
|
||||
/// "https://en.wikipedia.org/wiki/Rust_(video_game)" (include parens)
|
||||
/// "(https://example.com)" (do not include the parens)
|
||||
///
|
||||
/// There are many complicated cases where these heuristics break down, but
|
||||
/// handling them well requires a non-regex approach.
|
||||
pub const REGEX: &str = concatcp!(
|
||||
SCHEME_URL_BRANCH,
|
||||
"|",
|
||||
ROOTED_OR_RELATIVE_PATH_BRANCH,
|
||||
"|",
|
||||
BARE_RELATIVE_PATH_BRANCH
|
||||
);
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use onig::Regex;
|
||||
|
||||
#[test]
|
||||
fn url_regex() {
|
||||
let re = Regex::new(REGEX).unwrap();
|
||||
|
||||
let cases: &[(&str, &str)] = &[
|
||||
("hello https://example.com world", "https://example.com"),
|
||||
(
|
||||
"https://example.com/foo(bar) more",
|
||||
"https://example.com/foo(bar)",
|
||||
),
|
||||
(
|
||||
"https://example.com/foo(bar)baz more",
|
||||
"https://example.com/foo(bar)baz",
|
||||
),
|
||||
(
|
||||
"Link inside (https://example.com) parens",
|
||||
"https://example.com",
|
||||
),
|
||||
(
|
||||
"Link period https://example.com. More text.",
|
||||
"https://example.com",
|
||||
),
|
||||
(
|
||||
"Link trailing comma https://example.com, more text.",
|
||||
"https://example.com",
|
||||
),
|
||||
(
|
||||
"Link in double quotes \"https://example.com\" and more",
|
||||
"https://example.com",
|
||||
),
|
||||
(
|
||||
"Link in single quotes 'https://example.com' and more",
|
||||
"https://example.com",
|
||||
),
|
||||
(
|
||||
"some file with https://google.com https://duckduckgo.com links.",
|
||||
"https://google.com",
|
||||
),
|
||||
(
|
||||
"and links in it. links https://yahoo.com mailto:test@example.com ssh://1.2.3.4",
|
||||
"https://yahoo.com",
|
||||
),
|
||||
(
|
||||
"also match http://example.com non-secure links",
|
||||
"http://example.com",
|
||||
),
|
||||
(
|
||||
"match tel://+12123456789 phone numbers",
|
||||
"tel://+12123456789",
|
||||
),
|
||||
(
|
||||
"match with query url https://example.com?query=1&other=2 and more text.",
|
||||
"https://example.com?query=1&other=2",
|
||||
),
|
||||
(
|
||||
"url with dashes [mode 2027](https://github.com/contour-terminal/terminal-unicode-core) for better unicode support",
|
||||
"https://github.com/contour-terminal/terminal-unicode-core",
|
||||
),
|
||||
("dot.http://example.com", "http://example.com"),
|
||||
(
|
||||
"weird characters https://example.com/~user/?query=1&other=2#hash and more",
|
||||
"https://example.com/~user/?query=1&other=2#hash",
|
||||
),
|
||||
(
|
||||
"square brackets https://example.com/[foo] and more",
|
||||
"https://example.com/[foo]",
|
||||
),
|
||||
(
|
||||
"[13]:TooManyStatements: TempFile#assign_temp_file_to_entity has approx 7 statements [https://example.com/docs/Too-Many-Statements.md]",
|
||||
"https://example.com/docs/Too-Many-Statements.md",
|
||||
),
|
||||
("match ftp://example.com ftp links", "ftp://example.com"),
|
||||
("match file://example.com file links", "file://example.com"),
|
||||
("match ssh://example.com ssh links", "ssh://example.com"),
|
||||
("match git://example.com git links", "git://example.com"),
|
||||
("/tmp/test.txt http://www.google.com", "/tmp/test.txt"),
|
||||
("match tel:+18005551234 tel links", "tel:+18005551234"),
|
||||
(
|
||||
"match magnet:?xt=urn:btih:1234567890 magnet links",
|
||||
"magnet:?xt=urn:btih:1234567890",
|
||||
),
|
||||
(
|
||||
"match ipfs://QmSomeHashValue ipfs links",
|
||||
"ipfs://QmSomeHashValue",
|
||||
),
|
||||
(
|
||||
"match ipns://QmSomeHashValue ipns links",
|
||||
"ipns://QmSomeHashValue",
|
||||
),
|
||||
(
|
||||
"match gemini://example.com gemini links",
|
||||
"gemini://example.com",
|
||||
),
|
||||
(
|
||||
"match gopher://example.com gopher links",
|
||||
"gopher://example.com",
|
||||
),
|
||||
(
|
||||
"match news:comp.infosystems.www.servers.unix news links",
|
||||
"news:comp.infosystems.www.servers.unix",
|
||||
),
|
||||
(
|
||||
"/Users/ghostty.user/code/example.py",
|
||||
"/Users/ghostty.user/code/example.py",
|
||||
),
|
||||
(
|
||||
"/Users/ghostty.user/code/../example.py",
|
||||
"/Users/ghostty.user/code/../example.py",
|
||||
),
|
||||
(
|
||||
"/Users/ghostty.user/code/../example.py hello world",
|
||||
"/Users/ghostty.user/code/../example.py",
|
||||
),
|
||||
("../example.py", "../example.py"),
|
||||
("../example.py ", "../example.py "),
|
||||
("first time ../example.py contributor ", "../example.py"),
|
||||
(
|
||||
"[link](/home/user/ghostty.user/example)",
|
||||
"/home/user/ghostty.user/example",
|
||||
),
|
||||
(
|
||||
"Serving HTTP on :: port 8000 (http://[::]:8000/)",
|
||||
"http://[::]:8000/",
|
||||
),
|
||||
(
|
||||
"IPv6 address https://[2001:db8::1]:8080/path",
|
||||
"https://[2001:db8::1]:8080/path",
|
||||
),
|
||||
("IPv6 localhost http://[::1]:3000", "http://[::1]:3000"),
|
||||
(
|
||||
"Complex IPv6 https://[2001:db8:85a3:8d3:1319:8a2e:370:7348]:443/",
|
||||
"https://[2001:db8:85a3:8d3:1319:8a2e:370:7348]:443/",
|
||||
),
|
||||
(
|
||||
"IPv6 with path https://[2001:db8::1]/path/to/resource",
|
||||
"https://[2001:db8::1]/path/to/resource",
|
||||
),
|
||||
(
|
||||
"IPv6 with query https://[2001:db8::1]:8080/api?param=value&other=123",
|
||||
"https://[2001:db8::1]:8080/api?param=value&other=123",
|
||||
),
|
||||
(
|
||||
"IPv6 compressed http://[2001:db8::]:80/",
|
||||
"http://[2001:db8::]:80/",
|
||||
),
|
||||
(
|
||||
"IPv6 multiple zeros http://[2001:0:0:0:0:0:0:1]",
|
||||
"http://[2001:0:0:0:0:0:0:1]",
|
||||
),
|
||||
(
|
||||
"IPv6 link-local https://[fe80::1234:5678:9abc]",
|
||||
"https://[fe80::1234:5678:9abc]",
|
||||
),
|
||||
(
|
||||
"IPv6 multicast http://[ff02::1]/stream",
|
||||
"http://[ff02::1]/stream",
|
||||
),
|
||||
(
|
||||
"IPv6 in markdown [link](http://[2001:db8::1]/docs)",
|
||||
"http://[2001:db8::1]/docs",
|
||||
),
|
||||
("./spaces-end. ", "./spaces-end. "),
|
||||
("./space middle", "./space middle"),
|
||||
("../test folder/file.txt", "../test folder/file.txt"),
|
||||
("/tmp/test folder/file.txt", "/tmp/test folder/file.txt"),
|
||||
("/tmp/test folder/file.txt", "/tmp/test"),
|
||||
(
|
||||
"diff --git a/src/font/shaper/harfbuzz.zig b/src/font/shaper/harfbuzz.zig",
|
||||
"a/src/font/shaper/harfbuzz.zig",
|
||||
),
|
||||
("/tmp/foo /tmp/bar", "/tmp/foo"),
|
||||
("/tmp/foo.txt /tmp/bar.txt", "/tmp/foo.txt"),
|
||||
("src/config/url.zig", "src/config/url.zig"),
|
||||
("app/folder/file.rb:1", "app/folder/file.rb:1"),
|
||||
("modified: src/config/url.zig", "src/config/url.zig"),
|
||||
(
|
||||
"lib/ghostty/terminal.zig:42:10",
|
||||
"lib/ghostty/terminal.zig:42:10",
|
||||
),
|
||||
("some-pkg/src/file.txt more text", "some-pkg/src/file.txt"),
|
||||
("src/foo.c,baz.txt", "src/foo.c"),
|
||||
("~/foo/bar.txt", "~/foo/bar.txt"),
|
||||
("open ~/Documents/notes.md please", "~/Documents/notes.md"),
|
||||
("~/.config/ghostty/config", "~/.config/ghostty/config"),
|
||||
(
|
||||
"directory: ~/src/ghostty-org/ghostty",
|
||||
"~/src/ghostty-org/ghostty",
|
||||
),
|
||||
("$HOME/src/config/url.zig", "$HOME/src/config/url.zig"),
|
||||
(
|
||||
"project dir: $PWD/src/ghostty/main.zig",
|
||||
"$PWD/src/ghostty/main.zig",
|
||||
),
|
||||
("foo/$BAR/baz", "foo/$BAR/baz"),
|
||||
(".foo/bar/$VAR", ".foo/bar/$VAR"),
|
||||
(".config/ghostty/config", ".config/ghostty/config"),
|
||||
(
|
||||
"loaded from .local/share/ghostty/state.db now",
|
||||
".local/share/ghostty/state.db",
|
||||
),
|
||||
("../some/where", "../some/where"),
|
||||
(
|
||||
" - shared/src/foo/SomeItem.m:12, shared/src/",
|
||||
"shared/src/foo/SomeItem.m:12",
|
||||
),
|
||||
("foo.local/share", "foo.local/share"),
|
||||
("2024/report.txt", "2024/report.txt"),
|
||||
("./foo bar,baz", "./foo bar"),
|
||||
("/tmp/foo bar,baz", "/tmp/foo bar"),
|
||||
(
|
||||
"./.config/ghostty: Needs upstream (main)",
|
||||
"./.config/ghostty",
|
||||
),
|
||||
("./Downloads: Operation not permitted", "./Downloads"),
|
||||
];
|
||||
|
||||
for (input, expect) in cases {
|
||||
let mut it = re.find_iter(input);
|
||||
let (s, e) = it
|
||||
.next()
|
||||
.unwrap_or_else(|| panic!("no match for {input:?}"));
|
||||
assert_eq!(&input[s..e], *expect, "input: {input:?}");
|
||||
}
|
||||
|
||||
let no_match_cases: &[&str] = &[
|
||||
"input/output",
|
||||
"foo/bar",
|
||||
"$10/bar",
|
||||
"$10/$20",
|
||||
"$10/bar.txt",
|
||||
"foo/bar,baz.txt",
|
||||
"foo$BAR/baz.txt",
|
||||
"foo~/bar.txt",
|
||||
"// foo bar",
|
||||
"//foo",
|
||||
];
|
||||
for input in no_match_cases {
|
||||
assert!(re.find(input).is_none(), "unexpected match for {input:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user