1
1
mirror of https://github.com/wez/wezterm.git synced 2024-11-26 08:25:50 +03:00

improve default hyperlink_rules. add default_hyperlink_rules()

refs: https://github.com/wez/wezterm/issues/928
This commit is contained in:
Wez Furlong 2023-03-16 20:41:29 -07:00
parent c093b8861a
commit 6a9dfc409d
No known key found for this signature in database
GPG Key ID: 7A7F66A31EC9B387
7 changed files with 186 additions and 64 deletions

View File

@ -1438,14 +1438,18 @@ fn default_initial_cols() -> u16 {
80 80
} }
fn default_hyperlink_rules() -> Vec<hyperlink::Rule> { pub fn default_hyperlink_rules() -> Vec<hyperlink::Rule> {
vec![ vec![
// URL with a protocol // First handle URLs wrapped with punctuation (i.e. brackets)
hyperlink::Rule::new(r"\b\w+://[\w.-]+\.[a-z]{2,15}\S*\b", "$0").unwrap(), // e.g. [http://foo] (http://foo) <http://foo>
hyperlink::Rule::with_highlight(r"\((\w+://\S+)\)", "$1", 1).unwrap(),
hyperlink::Rule::with_highlight(r"\[(\w+://\S+)\]", "$1", 1).unwrap(),
hyperlink::Rule::with_highlight(r"<(\w+://\S+)>", "$1", 1).unwrap(),
// Then handle URLs not wrapped in brackets
// and include terminating ), / or - characters, if any
hyperlink::Rule::new(r"\b\w+://\S+[)/a-zA-Z0-9-]+", "$0").unwrap(),
// implicit mailto link // implicit mailto link
hyperlink::Rule::new(r"\b\w+@[\w-]+(\.[\w-]+)+\b", "mailto:$0").unwrap(), hyperlink::Rule::new(r"\b\w+@[\w-]+(\.[\w-]+)+\b", "mailto:$0").unwrap(),
// file://
hyperlink::Rule::new(r"\bfile://\S*\b", "$0").unwrap(),
] ]
} }

View File

@ -353,6 +353,14 @@ end
wezterm_mod.set("shell_quote_arg", lua.create_function(shell_quote_arg)?)?; wezterm_mod.set("shell_quote_arg", lua.create_function(shell_quote_arg)?)?;
wezterm_mod.set("shell_split", lua.create_function(shell_split)?)?; wezterm_mod.set("shell_split", lua.create_function(shell_split)?)?;
wezterm_mod.set(
"default_hyperlink_rules",
lua.create_function(move |lua, ()| {
let rules = crate::config::default_hyperlink_rules();
Ok(to_lua(lua, rules))
})?,
)?;
// Define our own os.getenv function that knows how to resolve current // Define our own os.getenv function that knows how to resolve current
// environment values from eg: the registry on Windows, or for // environment values from eg: the registry on Windows, or for
// the current SHELL value on unix, even if the user has changed // the current SHELL value on unix, even if the user has changed

View File

@ -79,6 +79,10 @@ As features stabilize some brief notes about them will accumulate here.
[#1485](https://github.com/wez/wezterm/issues/1485) [#1485](https://github.com/wez/wezterm/issues/1485)
* `wezterm sssh` now supports `%l` and `%L` tokens in config files. * `wezterm sssh` now supports `%l` and `%L` tokens in config files.
[#3176](https://github.com/wez/wezterm/issues/3176) [#3176](https://github.com/wez/wezterm/issues/3176)
* [hyperlink_rules](config/lua/config/hyperlink_rules.md) now support
specifying which capture group should be highlighted.
* [wezterm.default_hyperlink_rules](config/lua/wezterm/default_hyperlink_rules.md)
function makes it easier to extend the default set of hyperlink rules.
#### Fixed #### Fixed
* X11: hanging or killing the IME could hang wezterm * X11: hanging or killing the IME could hang wezterm
@ -131,6 +135,9 @@ As features stabilize some brief notes about them will accumulate here.
[#3250](https://github.com/wez/wezterm/issues/3250) [#3250](https://github.com/wez/wezterm/issues/3250)
* Config was not applied to non-zoomed panes when config was reloaded * Config was not applied to non-zoomed panes when config was reloaded
[#3259](https://github.com/wez/wezterm/issues/3259) [#3259](https://github.com/wez/wezterm/issues/3259)
* Default [hyperlink_rules](config/lua/config/hyperlink_rules.md) now match
URLs with port numbers
[#928](https://github.com/wez/wezterm/issues/928)
#### Changed #### Changed
* `CTRL-SHIFT-P` now activates the new [command * `CTRL-SHIFT-P` now activates the new [command

View File

@ -3,5 +3,109 @@
Defines rules to match text from the terminal output and generate Defines rules to match text from the terminal output and generate
clickable links. clickable links.
See [Hyperlinks](../../../hyperlinks.md) for more information and The value is a list of rule entries. Each entry has the following fields:
examples.
* `regex` - the regular expression to match
* `format` - Controls what will be used to form the link. The string
can use placeholders like `$0`, `$1`, `$2` etc. that will be replaced
with that numbered capture group. So, `$0` will take the entire
region of text matched by the whole regex, while `$1` matches out
the first capture group. In the example below, `mailto:$0` is
used to prefix a protocol to the text to make it into an URL.
* `highlight` - (*Since: nightly builds only*) specifies the range
of the matched text that should be highlighted/underlined when
the mouse hovers over the link. The default is `0`, highlighting
the entire region of text matched by the regex.
Assigning `hyperlink_rules` overrides the built-in default rules.
The default value for `hyperlink_rules` can be retrieved using
[wezterm.default_hyperlink_rules()](../wezterm/default_hyperlink_rules.md),
and is shown below:
```lua
return {
hyperlink_rules = {
-- Matches: a URL in parens: (URL)
{
regex = '\\((\\w+://\\S+)\\)',
format = '$1',
highlight = 1,
},
-- Matches: a URL in brackets: [URL]
{
regex = '\\[(\\w+://\\S+)\\]',
format = '$1',
highlight = 1,
},
-- Matches: a URL in curly braces: {URL}
{
regex = '\\{(\\w+://\\S+)\\}',
format = '$1',
highlight = 1,
},
-- Matches: a URL in angle brackets: <URL>
{
regex = '<(\\w+://\\S+)>',
format = '$1',
highlight = 1,
},
-- Then handle URLs not wrapped in brackets
{
regex = '\\b\\w+://\\S+[)/a-zA-Z0-9-]+',
format = '$0',
},
-- implicit mailto link
{
regex = '\\b\\w+@[\\w-]+(\\.[\\w-]+)+\\b',
format = 'mailto:$0',
},
},
}
```
!!! note
In quoted Lua string literals the backslash character must be
quoted even if the following character isn't meaningful to Lua
when quoted by a backslash. That means that you'll always want to
double it up as `\\` when using it in a regex string.
Alternatively, you can use the alternative string literal
syntax; the following two examples are equivalent:
```lua
regex = [[\b[tT](\d+)\b]]
```
```lua
regex = '\\b[tT](\\d+)\\b'
```
Some other examples include:
```lua
local wezterm = require 'wezterm'
-- Use the defaults as a base
local hyperlink_rules = wezterm.default_hyperlink_rules()
-- make task numbers clickable
-- the first matched regex group is captured in $1.
table.insert(hyperlink_rules, {
regex = [[\b[tt](\d+)\b]],
format = 'https://example.com/tasks/?t=$1',
})
-- make username/project paths clickable. this implies paths like the following are for github.
-- ( "nvim-treesitter/nvim-treesitter" | wbthomason/packer.nvim | wez/wezterm | "wez/wezterm.git" )
-- as long as a full url hyperlink regex exists above this it should not match a full url to
-- github or gitlab / bitbucket (i.e. https://gitlab.com/user/project.git is still a whole clickable url)
table.insert(hyperlink_rules, {
regex = [[["]?([\w\d]{1}[-\w\d]+)(/){1}([-\w\d\.]+)["]?]],
format = 'https://www.github.com/$1/$3',
})
return {
hyperlink_rules = hyperlink_rules,
}
```

View File

@ -0,0 +1,6 @@
# `wezterm.default_hyperlink_rules()`
*Since: nightly builds only*
Returns the compiled-in default values for [hyperlink_rules](../config/hyperlink_rules.md).

View File

@ -5,66 +5,42 @@ wezterm has support for both implicit and explicit hyperlinks.
Implicit hyperlinks are produced by running a series of rules over the output Implicit hyperlinks are produced by running a series of rules over the output
displayed in the terminal to produce a hyperlink. There is a default rule displayed in the terminal to produce a hyperlink. There is a default rule
to match URLs and make them clickable, but you can also specify your own rules to match URLs and make them clickable, but you can also specify your own rules
to make your own links. As an example, at my place of work many of our internal to make your own links.
tools use `T123` to indicate task number 123 in our internal task tracking system.
It is desirable to make this clickable, and that can be done with the following As an example, at my place of work many of our internal tools use `T123` to
configuration in your `~/.wezterm.lua`: indicate task number 123 in our internal task tracking system. It is desirable
to make this clickable, and that can be done with the following configuration
in your `~/.wezterm.lua`:
```lua ```lua
local wezterm = require 'wezterm'
-- Use the defaults as a base
local hyperlink_rules = wezterm.default_hyperlink_rules()
-- make task numbers clickable
-- the first matched regex group is captured in $1.
table.insert(hyperlink_rules, {
regex = [[\b[tt](\d+)\b]],
format = 'https://example.com/tasks/?t=$1',
})
-- make username/project paths clickable. this implies paths like the following are for github.
-- ( "nvim-treesitter/nvim-treesitter" | wbthomason/packer.nvim | wez/wezterm | "wez/wezterm.git" )
-- as long as a full url hyperlink regex exists above this it should not match a full url to
-- github or gitlab / bitbucket (i.e. https://gitlab.com/user/project.git is still a whole clickable url)
table.insert(hyperlink_rules, {
regex = [[["]?([\w\d]{1}[-\w\d]+)(/){1}([-\w\d\.]+)["]?]],
format = 'https://www.github.com/$1/$3',
})
return { return {
hyperlink_rules = { hyperlink_rules = hyperlink_rules,
-- Linkify things that look like URLs and the host has a TLD name.
-- Compiled-in default. Used if you don't specify any hyperlink_rules.
{
regex = '\\b\\w+://[\\w.-]+\\.[a-z]{2,15}\\S*\\b',
format = '$0',
},
-- linkify email addresses
-- Compiled-in default. Used if you don't specify any hyperlink_rules.
{
regex = [[\b\w+@[\w-]+(\.[\w-]+)+\b]],
format = 'mailto:$0',
},
-- file:// URI
-- Compiled-in default. Used if you don't specify any hyperlink_rules.
{
regex = [[\bfile://\S*\b]],
format = '$0',
},
-- Linkify things that look like URLs with numeric addresses as hosts.
-- E.g. http://127.0.0.1:8000 for a local development server,
-- or http://192.168.1.1 for the web interface of many routers.
{
regex = [[\b\w+://(?:[\d]{1,3}\.){3}[\d]{1,3}\S*\b]],
format = '$0',
},
-- Make task numbers clickable
-- The first matched regex group is captured in $1.
{
regex = [[\b[tT](\d+)\b]],
format = 'https://example.com/tasks/?t=$1',
},
-- Make username/project paths clickable. This implies paths like the following are for GitHub.
-- ( "nvim-treesitter/nvim-treesitter" | wbthomason/packer.nvim | wez/wezterm | "wez/wezterm.git" )
-- As long as a full URL hyperlink regex exists above this it should not match a full URL to
-- GitHub or GitLab / BitBucket (i.e. https://gitlab.com/user/project.git is still a whole clickable URL)
{
regex = [[["]?([\w\d]{1}[-\w\d]+)(/){1}([-\w\d\.]+)["]?]],
format = 'https://www.github.com/$1/$3',
},
},
} }
``` ```
Note that it is generally convenient to use literal strings (`[[...]]`) See also [hyperlink_rules](config/lua/config/hyperlink_rules.md) and
when declaring your hyperlink rules, so you won't have to escape [default_hyperlink_rules](config/lua/wezterm/default_hyperlink_rules.md).
backslashes. In the example above, all cases except the first use
literal strings for their regular expressions.
### Explicit Hyperlinks ### Explicit Hyperlinks

View File

@ -160,6 +160,10 @@ pub struct Rule {
/// with ambiguous replacement of `$11` vs `$1` in the case of /// with ambiguous replacement of `$11` vs `$1` in the case of
/// more complex regexes. /// more complex regexes.
pub format: String, pub format: String,
/// Which capture to highlight
#[dynamic(default)]
pub highlight: usize,
} }
struct RegexWrap(Regex); struct RegexWrap(Regex);
@ -221,6 +225,7 @@ pub struct RuleMatch {
} }
/// An internal intermediate match result /// An internal intermediate match result
#[derive(Debug)]
struct Match<'t> { struct Match<'t> {
rule: &'t Rule, rule: &'t Rule,
captures: Captures<'t>, captures: Captures<'t>,
@ -229,16 +234,20 @@ struct Match<'t> {
impl<'t> Match<'t> { impl<'t> Match<'t> {
/// Returns the length of the matched text in bytes (not cells!) /// Returns the length of the matched text in bytes (not cells!)
fn len(&self) -> usize { fn len(&self) -> usize {
let c0 = self.captures.get(0).unwrap(); let c0 = self.highlight().unwrap();
c0.end() - c0.start() c0.end() - c0.start()
} }
/// Returns the span of the matched text, measured in bytes (not cells!) /// Returns the span of the matched text, measured in bytes (not cells!)
fn range(&self) -> Range<usize> { fn range(&self) -> Range<usize> {
let c0 = self.captures.get(0).unwrap(); let c0 = self.highlight().unwrap();
c0.start()..c0.end() c0.start()..c0.end()
} }
fn highlight(&self) -> Option<regex::Match> {
self.captures.get(self.rule.highlight)
}
/// Expand replacements in the format string to yield the URL /// Expand replacements in the format string to yield the URL
/// The replacement is as described on Rule::format. /// The replacement is as described on Rule::format.
fn expand(&self) -> String { fn expand(&self) -> String {
@ -260,9 +269,14 @@ impl<'t> Match<'t> {
impl Rule { impl Rule {
/// Construct a new rule. It may fail if the regex is invalid. /// Construct a new rule. It may fail if the regex is invalid.
pub fn new(regex: &str, format: &str) -> Result<Self> { pub fn new(regex: &str, format: &str) -> Result<Self> {
Self::with_highlight(regex, format, 0)
}
pub fn with_highlight(regex: &str, format: &str, highlight: usize) -> Result<Self> {
Ok(Self { Ok(Self {
regex: Regex::new(regex)?, regex: Regex::new(regex)?,
format: format.to_owned(), format: format.to_owned(),
highlight,
}) })
} }
@ -272,7 +286,10 @@ impl Rule {
let mut matches = Vec::new(); let mut matches = Vec::new();
for rule in rules.iter() { for rule in rules.iter() {
for captures in rule.regex.captures_iter(line) { for captures in rule.regex.captures_iter(line) {
matches.push(Match { rule, captures }); let m = Match { rule, captures };
if m.highlight().is_some() {
matches.push(m);
}
} }
} }
// Sort the matches by descending match length. // Sort the matches by descending match length.