From b904ed7677aae82e185c63113f5d87df6edeef63 Mon Sep 17 00:00:00 2001 From: Wez Furlong Date: Fri, 25 Aug 2023 06:46:44 -0700 Subject: [PATCH] breaking(ish): pane.get_current_working_dir now returns Url Previously we'd return the Url string. Now we provide a Url object that provides access to the various elements of the Url. This will cause slightly breakage for folks that were treating it as a string in their status event handlers, for example. The docs have been updated to show how to run with both this new Url object and also continue to run on older versions of wezterm. They now also show how to manually percent decode the url for older versions of wezterm. refs: https://github.com/wez/wezterm/discussions/4157 refs: https://github.com/wez/wezterm/issues/4000 --- .gitignore | 1 + Cargo.lock | 15 ++++ ci/generate-docs.py | 4 + docs/changelog.md | 8 ++ .../lua/pane/get_current_working_dir.md | 10 +++ docs/config/lua/wezterm.url/Url.md | 32 +++++++ docs/config/lua/wezterm.url/index.markdown | 10 +++ docs/config/lua/wezterm.url/parse.md | 7 ++ docs/config/lua/window/set_right_status.md | 41 ++++++--- env-bootstrap/Cargo.toml | 1 + env-bootstrap/src/lib.rs | 1 + lua-api-crates/mux/Cargo.toml | 1 + lua-api-crates/mux/src/pane.rs | 3 +- lua-api-crates/url-funcs/Cargo.toml | 14 ++++ lua-api-crates/url-funcs/src/lib.rs | 83 +++++++++++++++++++ wezterm-gui/Cargo.toml | 1 + wezterm-gui/src/termwindow/mod.rs | 10 +-- 17 files changed, 223 insertions(+), 19 deletions(-) create mode 100644 docs/config/lua/wezterm.url/Url.md create mode 100644 docs/config/lua/wezterm.url/index.markdown create mode 100644 docs/config/lua/wezterm.url/parse.md create mode 100644 lua-api-crates/url-funcs/Cargo.toml create mode 100644 lua-api-crates/url-funcs/src/lib.rs diff --git a/.gitignore b/.gitignore index 902f409c265..bb9982eb07e 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,7 @@ dhat-heap.json /docs/config/lua/wezterm.procinfo/index.md /docs/config/lua/wezterm.time/index.md /docs/config/lua/wezterm.time/Time/index.md +/docs/config/lua/wezterm.url/index.md /docs/config/lua/wezterm/index.md /docs/config/lua/window-events/index.md /docs/config/lua/window/index.md diff --git a/Cargo.lock b/Cargo.lock index 3e0f6e46d8c..9a441ca2dec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1506,6 +1506,7 @@ dependencies = [ "termwiz", "termwiz-funcs", "time-funcs", + "url-funcs", "wezterm-version", "winapi", ] @@ -3266,6 +3267,7 @@ dependencies = [ "portable-pty", "smol", "termwiz", + "url-funcs", "wezterm-dynamic", "wezterm-term", ] @@ -5671,6 +5673,18 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "url-funcs" +version = "0.1.0" +dependencies = [ + "anyhow", + "config", + "luahelper", + "percent-encoding", + "url", + "wezterm-dynamic", +] + [[package]] name = "utf8parse" version = "0.2.1" @@ -6185,6 +6199,7 @@ dependencies = [ "unicode-segmentation", "unicode-width", "url", + "url-funcs", "walkdir", "wezterm-bidi", "wezterm-blob-leases", diff --git a/ci/generate-docs.py b/ci/generate-docs.py index 9794adfc0a0..a4d15fc96e4 100644 --- a/ci/generate-docs.py +++ b/ci/generate-docs.py @@ -398,6 +398,10 @@ def render(self, output, depth=0, mode="mdbook"): "module: wezterm.time", "config/lua/wezterm.time", ), + Gen( + "module: wezterm.url", + "config/lua/wezterm.url", + ), Gen( "enum: KeyAssignment", "config/lua/keyassignment", diff --git a/docs/changelog.md b/docs/changelog.md index d8c6635ba05..452bd6c6cb0 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -23,6 +23,14 @@ As features stabilize some brief notes about them will accumulate here. #### Changed * The default for [front_end](config/lua/config/front_end.md) is now `WebGpu`. +* The return type of + [pane.get_current_working_dir](config/lua/pane/get_current_working_dir.md) + and [PaneInformation.current_working_dir](config/lua/PaneInformation.md) + has changed to the new [Url](config/lua/wezterm.url/Url.md) object, which + makes it easier to handle things like percent-encoding for paths with spaces + or non-ASCII characters. Please see the revised example on + [set_right_status](config/lua/window/set_right_status.md) for example usage + with backwards compatibility in mind. #4000 * Added split out github short codes from the various charselect sections into their own new Short Codes section. * CharSelect now shows emoji variations such as skin tones diff --git a/docs/config/lua/pane/get_current_working_dir.md b/docs/config/lua/pane/get_current_working_dir.md index c631f512bf6..d0b74cfb120 100644 --- a/docs/config/lua/pane/get_current_working_dir.md +++ b/docs/config/lua/pane/get_current_working_dir.md @@ -24,3 +24,13 @@ working directory using operating system dependent code: If the current working directory is not known then this method returns `nil`. Otherwise, it returns the current working directory as a URI string. +Note that while the current working directory is usually a file path, +it is possible for an application to set it to an FTP URL or some +other kind of URL, which is why this method doesn't simply return +a file path string. + +{{since('nightly')}} + +This method now returns a [Url](../wezterm.url/Url.md) object which +provides a convenient way to decode and operate on the URL. + diff --git a/docs/config/lua/wezterm.url/Url.md b/docs/config/lua/wezterm.url/Url.md new file mode 100644 index 00000000000..e57450af223 --- /dev/null +++ b/docs/config/lua/wezterm.url/Url.md @@ -0,0 +1,32 @@ +# Url object + +{{since('nightly')}} + +The `Url` object represents a parsed Url. It has the following fields: + +* `scheme` - the URL scheme such as `"file"`, or `"https"` +* `file_path` - decodes the `path` field and interprets it as a file path +* `username` - the username portion of the URL, or an empty string if none is specified +* `password` - the password portion of the URL, or `nil` if none is specified +* `host` - the hostname portion of the URL, with IDNA decoded to UTF-8 +* `path` - the path portion of the URL, complete with percent encoding +* `fragment` - the fragment portion of the URL +* `query` - the query portion of the URL + +```lua +local wezterm = require 'wezterm' + +local url = wezterm.url.parse 'file://myhost/some/path%20with%20spaces' +assert(url.scheme == 'file') +assert(url.file_path == '/some/path with spaces') + +local url = + wezterm.url.parse 'https://github.com/rust-lang/rust/issues?labels=E-easy&state=open' +assert(url.scheme == 'https') +assert(url.username == '') +assert(url.password == nil) +assert(url.host == 'github.com') +assert(url.path == '/rust-lang/rust/issues') +assert(url.query == 'labels=E-easy&state=open') +``` + diff --git a/docs/config/lua/wezterm.url/index.markdown b/docs/config/lua/wezterm.url/index.markdown new file mode 100644 index 00000000000..92a56eef37f --- /dev/null +++ b/docs/config/lua/wezterm.url/index.markdown @@ -0,0 +1,10 @@ +# `wezterm.url` module + +{{since('nightly')}} + +The `wezterm.url` module exposes functions that allow working +with URLs. + +## Available functions and objects + + diff --git a/docs/config/lua/wezterm.url/parse.md b/docs/config/lua/wezterm.url/parse.md new file mode 100644 index 00000000000..aae96dd2bd4 --- /dev/null +++ b/docs/config/lua/wezterm.url/parse.md @@ -0,0 +1,7 @@ +# `wezterm.url.parse(URL_STRING)` + +{{since('nightly')}} + +Attempts to parse the provided *URL_STRING* as a URL. +If success, returns a [Url](Url.md) object representing that URL. + diff --git a/docs/config/lua/window/set_right_status.md b/docs/config/lua/window/set_right_status.md index a77ca815998..4010e8247ff 100644 --- a/docs/config/lua/window/set_right_status.md +++ b/docs/config/lua/window/set_right_status.md @@ -51,23 +51,40 @@ wezterm.on('update-right-status', function(window, pane) -- shell is using OSC 7 on the remote host. local cwd_uri = pane:get_current_working_dir() if cwd_uri then - cwd_uri = cwd_uri:sub(8) - local slash = cwd_uri:find '/' local cwd = '' local hostname = '' - if slash then - hostname = cwd_uri:sub(1, slash - 1) - -- Remove the domain name portion of the hostname - local dot = hostname:find '[.]' - if dot then - hostname = hostname:sub(1, dot - 1) + + if type(cwd_uri) == 'userdata' then + -- Running on a newer version of wezterm and we have + -- a URL object here, making this simple! + + cwd = cwd_uri.file_path + hostname = cwd_uri.host or wezterm.hostname() + else + -- an older version of wezterm, 20230712-072601-f4abf8fd or earlier, + -- which doesn't have the Url object + cwd_uri = cwd_uri:sub(8) + local slash = cwd_uri:find '/' + if slash then + hostname = cwd_uri:sub(1, slash - 1) + -- and extract the cwd from the uri, decoding %-encoding + cwd = cwd_uri:sub(slash):gsub('%%(%x%x)', function(hex) + return string.char(tonumber(hex, 16)) + end) end - -- and extract the cwd from the uri - cwd = cwd_uri:sub(slash) + end - table.insert(cells, cwd) - table.insert(cells, hostname) + -- Remove the domain name portion of the hostname + local dot = hostname:find '[.]' + if dot then + hostname = hostname:sub(1, dot - 1) end + if hostname == '' then + hostname = wezterm.hostname() + end + + table.insert(cells, cwd) + table.insert(cells, hostname) end -- I like my date/time in this style: "Wed Mar 3 08:14" diff --git a/env-bootstrap/Cargo.toml b/env-bootstrap/Cargo.toml index 798dacb1209..f2bb155a00f 100644 --- a/env-bootstrap/Cargo.toml +++ b/env-bootstrap/Cargo.toml @@ -29,6 +29,7 @@ share-data = { path = "../lua-api-crates/share-data" } ssh-funcs = { path = "../lua-api-crates/ssh-funcs" } spawn-funcs = { path = "../lua-api-crates/spawn-funcs" } time-funcs = { path = "../lua-api-crates/time-funcs" } +url-funcs = { path = "../lua-api-crates/url-funcs" } wezterm-version = { path = "../wezterm-version" } [target."cfg(windows)".dependencies] diff --git a/env-bootstrap/src/lib.rs b/env-bootstrap/src/lib.rs index f80a87330f5..ebe6751d62d 100644 --- a/env-bootstrap/src/lib.rs +++ b/env-bootstrap/src/lib.rs @@ -205,6 +205,7 @@ fn register_lua_modules() { spawn_funcs::register, share_data::register, time_funcs::register, + url_funcs::register, ] { config::lua::add_context_setup_func(func); } diff --git a/lua-api-crates/mux/Cargo.toml b/lua-api-crates/mux/Cargo.toml index e0f4337931f..320af6ae50a 100644 --- a/lua-api-crates/mux/Cargo.toml +++ b/lua-api-crates/mux/Cargo.toml @@ -18,3 +18,4 @@ portable-pty = { path = "../../pty" } smol = "1.2" termwiz = { path = "../../termwiz" } mux = { path = "../../mux" } +url-funcs = { path = "../url-funcs" } diff --git a/lua-api-crates/mux/src/pane.rs b/lua-api-crates/mux/src/pane.rs index 60be9790dda..0c7b948dd79 100644 --- a/lua-api-crates/mux/src/pane.rs +++ b/lua-api-crates/mux/src/pane.rs @@ -4,6 +4,7 @@ use mlua::Value; use std::cmp::Ordering; use std::sync::Arc; use termwiz::cell::SemanticType; +use url_funcs::Url; use wezterm_term::{SemanticZone, StableRowIndex}; #[derive(Clone, Copy, Debug)] @@ -145,7 +146,7 @@ impl UserData for MuxPane { methods.add_method("get_current_working_dir", |_, this, _: ()| { let mux = get_mux()?; let pane = this.resolve(&mux)?; - Ok(pane.get_current_working_dir().map(|u| u.to_string())) + Ok(pane.get_current_working_dir().map(|url| Url { url })) }); methods.add_method("get_metadata", |lua, this, _: ()| { diff --git a/lua-api-crates/url-funcs/Cargo.toml b/lua-api-crates/url-funcs/Cargo.toml new file mode 100644 index 00000000000..37e5f370b8c --- /dev/null +++ b/lua-api-crates/url-funcs/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "url-funcs" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0" +config = { path = "../../config" } +luahelper = { path = "../../luahelper" } +percent-encoding = "2.3" +url = "2" +wezterm-dynamic = { path = "../../wezterm-dynamic" } diff --git a/lua-api-crates/url-funcs/src/lib.rs b/lua-api-crates/url-funcs/src/lib.rs new file mode 100644 index 00000000000..3e8a9a2d8d4 --- /dev/null +++ b/lua-api-crates/url-funcs/src/lib.rs @@ -0,0 +1,83 @@ +use crate::mlua::UserDataFields; +use config::lua::get_or_create_sub_module; +use config::lua::mlua::{self, Lua, MetaMethod, UserData, UserDataMethods}; +use percent_encoding::percent_decode; + +pub fn register(lua: &Lua) -> anyhow::Result<()> { + let url_mod = get_or_create_sub_module(lua, "url")?; + + url_mod.set( + "parse", + lua.create_function(|_, s: String| { + let url = url::Url::parse(&s).map_err(|err| { + mlua::Error::external(format!("{err:#} while parsing {s} as URL")) + })?; + Ok(Url { url }) + })?, + )?; + + Ok(()) +} + +#[derive(Clone, Debug)] +pub struct Url { + pub url: url::Url, +} + +impl std::ops::Deref for Url { + type Target = url::Url; + fn deref(&self) -> &url::Url { + &self.url + } +} + +impl std::ops::DerefMut for Url { + fn deref_mut(&mut self) -> &mut url::Url { + &mut self.url + } +} + +impl UserData for Url { + fn add_methods<'lua, M: UserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_meta_method(MetaMethod::ToString, |_, this, _: ()| { + Ok(this.url.as_str().to_string()) + }); + } + + fn add_fields<'lua, F: UserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("scheme", |_, this| Ok(this.scheme().to_string())); + fields.add_field_method_get("username", |_, this| Ok(this.username().to_string())); + fields.add_field_method_get("password", |_, this| { + Ok(this.password().map(|s| s.to_string())) + }); + fields.add_field_method_get("host", |_, this| Ok(this.host_str().map(|s| s.to_string()))); + fields.add_field_method_get("port", |_, this| Ok(this.port())); + fields.add_field_method_get("query", |_, this| Ok(this.query().map(|s| s.to_string()))); + fields.add_field_method_get("fragment", |_, this| { + Ok(this.fragment().map(|s| s.to_string())) + }); + fields.add_field_method_get("path", |_, this| Ok(this.path().to_string())); + fields.add_field_method_get("file_path", |lua, this| { + if let Some(segments) = this.path_segments() { + let mut bytes = vec![]; + for segment in segments { + bytes.push(b'/'); + bytes.extend(percent_decode(segment.as_bytes())); + } + + // A windows drive letter must end with a slash. + if bytes.len() > 2 + && bytes[bytes.len() - 2].is_ascii_alphabetic() + && matches!(bytes[bytes.len() - 1], b':' | b'|') + { + bytes.push(b'/'); + } + + let s = lua.create_string(bytes)?; + Ok(Some(s)) + } else { + Ok(None) + } + }); + } +} diff --git a/wezterm-gui/Cargo.toml b/wezterm-gui/Cargo.toml index f8bd3a0cbe1..91f5b9d1d05 100644 --- a/wezterm-gui/Cargo.toml +++ b/wezterm-gui/Cargo.toml @@ -92,6 +92,7 @@ unicode-normalization = "0.1" unicode-segmentation = "1.8" unicode-width = "0.1" url = "2" +url-funcs = { path = "../lua-api-crates/url-funcs" } walkdir = "2" wezterm-bidi = { path = "../bidi" } wezterm-blob-leases = { path = "../wezterm-blob-leases", version="0.1", features=["simple_tempdir"] } diff --git a/wezterm-gui/src/termwindow/mod.rs b/wezterm-gui/src/termwindow/mod.rs index b5aa19fa76b..8106f4f5fcd 100644 --- a/wezterm-gui/src/termwindow/mod.rs +++ b/wezterm-gui/src/termwindow/mod.rs @@ -300,16 +300,14 @@ impl UserData for PaneInformation { Ok(name) }); fields.add_field_method_get("current_working_dir", |_, this| { - let mut name = None; if let Some(mux) = Mux::try_get() { if let Some(pane) = mux.get_pane(this.pane_id) { - name = pane.get_current_working_dir().map(|u| u.to_string()); + return Ok(pane + .get_current_working_dir() + .map(|url| url_funcs::Url { url })); } } - match name { - Some(name) => Ok(name), - None => Ok("".to_string()), - } + Ok(None) }); fields.add_field_method_get("domain_name", |_, this| { let mut name = None;