mirror of
https://github.com/wez/wezterm.git
synced 2024-11-22 22:42:48 +03:00
wezterm: allow overriding the default open-uri event
This builds on the new lua event handler plumbing added
in ccea650a93
to co-opt
the default URI opening action:
```lua
wezterm.on("open-uri", function(uri)
if uri:find("jira") then
wezterm.log_error("do something with jira")
wezterm.run_child_process({
"wezterm",
"start",
"--",
"jira",
"view",
extract_task_from_uri(uri)
})
-- prevent the default action from opening in a browser
return false
else
-- log but allow the uri to be opened in the browser
wezterm.log_error("clicken " .. uri)
end
end)
```
This doesn't allow exactly the sketched out option from
issue #223 to be implemented, but may be close enough
to be useful.
refs: #223
refs: #225
This commit is contained in:
parent
882f47f4ad
commit
9397f2a2db
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -696,6 +696,7 @@ dependencies = [
|
||||
"notify",
|
||||
"portable-pty",
|
||||
"pretty_env_logger",
|
||||
"promise",
|
||||
"serde",
|
||||
"smol",
|
||||
"termwiz",
|
||||
|
@ -28,6 +28,7 @@ notify = "4.0"
|
||||
portable-pty = { path = "../pty", features = ["serde_support"]}
|
||||
serde = {version="1.0", features = ["rc", "derive"]}
|
||||
smol = "1.2"
|
||||
promise = { path = "../promise" }
|
||||
termwiz = { path = "../termwiz" }
|
||||
toml = "0.5"
|
||||
wezterm-term = { path = "../term", features=["use_serde"] }
|
||||
|
@ -7,7 +7,10 @@ use luahelper::impl_lua_conversion;
|
||||
use mlua::Lua;
|
||||
use portable_pty::{CommandBuilder, PtySize};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smol::channel::{Receiver, Sender};
|
||||
use smol::prelude::*;
|
||||
use std;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::{OsStr, OsString};
|
||||
use std::fs;
|
||||
@ -16,6 +19,7 @@ use std::io::prelude::*;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::DirBuilderExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::rc::Rc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
use termwiz::hyperlink;
|
||||
@ -58,6 +62,123 @@ lazy_static! {
|
||||
static ref MAKE_LUA: Mutex<Option<LuaFactory>> = Mutex::new(Some(lua::make_lua_context));
|
||||
static ref SHOW_ERROR: Mutex<Option<ErrorCallback>> =
|
||||
Mutex::new(Some(|e| log::error!("{}", e)));
|
||||
static ref LUA_PIPE: LuaPipe = LuaPipe::new();
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static LUA_CONFIG: RefCell<Option<LuaConfigState>> = RefCell::new(None);
|
||||
}
|
||||
|
||||
struct LuaPipe {
|
||||
sender: Sender<mlua::Lua>,
|
||||
receiver: Receiver<mlua::Lua>,
|
||||
}
|
||||
impl LuaPipe {
|
||||
pub fn new() -> Self {
|
||||
let (sender, receiver) = smol::channel::unbounded();
|
||||
Self { sender, receiver }
|
||||
}
|
||||
}
|
||||
|
||||
/// The implementation is only slightly crazy...
|
||||
/// `Lua` is Send but !Sync.
|
||||
/// We take care to reference this only from the main thread of
|
||||
/// the application.
|
||||
/// We also need to take care to keep this `lua` alive if a long running
|
||||
/// future is outstanding while a config reload happens.
|
||||
/// We have to use `Rc` to manage its lifetime, but due to some issues
|
||||
/// with rust's async lifetime tracking we need to indirectly schedule
|
||||
/// some of the futures to avoid it thinking that the generated future
|
||||
/// in the async block needs to be Send.
|
||||
///
|
||||
/// A further complication is that config reloading tends to happen in
|
||||
/// a background filesystem watching thread.
|
||||
///
|
||||
/// The result of all these constraints is that the LuaPipe struct above
|
||||
/// is used as a channel to transport newly loaded lua configs to the
|
||||
/// main thread.
|
||||
///
|
||||
/// The main thread pops the loaded configs to obtain the latest one
|
||||
/// and updates LuaConfigState
|
||||
struct LuaConfigState {
|
||||
lua: Option<Rc<mlua::Lua>>,
|
||||
}
|
||||
|
||||
impl LuaConfigState {
|
||||
/// Consume any lua contexts sent to us via the
|
||||
/// config loader until we end up with the most
|
||||
/// recent one being referenced by LUA_CONFIG.
|
||||
fn update_to_latest(&mut self) {
|
||||
while let Ok(lua) = LUA_PIPE.receiver.try_recv() {
|
||||
self.lua.replace(Rc::new(lua));
|
||||
}
|
||||
}
|
||||
|
||||
/// Take a reference on the latest generation of the lua context
|
||||
fn get_lua(&self) -> Option<Rc<mlua::Lua>> {
|
||||
self.lua.as_ref().map(|lua| Rc::clone(lua))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn designate_this_as_the_main_thread() {
|
||||
LUA_CONFIG.with(|lc| {
|
||||
let mut lc = lc.borrow_mut();
|
||||
if lc.is_none() {
|
||||
lc.replace(LuaConfigState { lua: None });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Spawn a future that will run with an optional Lua state from the most
|
||||
/// recently loaded lua configuration.
|
||||
/// The `func` argument is passed the lua state and must return a Future.
|
||||
///
|
||||
/// This function MUST only be called from the main thread.
|
||||
/// In exchange for the caller checking for this, the parameters to
|
||||
/// this method are not required to be Send.
|
||||
///
|
||||
/// Calling this function from a secondary thread will panic.
|
||||
/// You should use `with_lua_config` if you are triggering a
|
||||
/// call from a secondary thread.
|
||||
pub async fn with_lua_config_on_main_thread<F, RETF, RET>(func: F) -> anyhow::Result<RET>
|
||||
where
|
||||
F: Fn(Option<Rc<mlua::Lua>>) -> RETF,
|
||||
RETF: Future<Output = anyhow::Result<RET>>,
|
||||
{
|
||||
let lua = LUA_CONFIG.with(|lc| {
|
||||
let mut lc = lc.borrow_mut();
|
||||
let lc = lc.as_mut().expect(
|
||||
"with_lua_config_on_main_thread not called
|
||||
from main thread, use with_lua_config instead!",
|
||||
);
|
||||
lc.update_to_latest();
|
||||
lc.get_lua()
|
||||
});
|
||||
|
||||
func(lua).await
|
||||
}
|
||||
|
||||
fn schedule_with_lua<F, RETF, RET>(func: F) -> promise::spawn::Task<anyhow::Result<RET>>
|
||||
where
|
||||
F: 'static,
|
||||
RET: 'static,
|
||||
F: Fn(Option<Rc<mlua::Lua>>) -> RETF,
|
||||
RETF: Future<Output = anyhow::Result<RET>>,
|
||||
{
|
||||
promise::spawn::spawn(async move { with_lua_config_on_main_thread(func).await })
|
||||
}
|
||||
|
||||
/// Spawn a future that will run with an optional Lua state from the most
|
||||
/// recently loaded lua configuration.
|
||||
/// The `func` argument is passed the lua state and must return a Future.
|
||||
pub async fn with_lua_config<F, RETF, RET>(func: F) -> anyhow::Result<RET>
|
||||
where
|
||||
F: Fn(Option<Rc<mlua::Lua>>) -> RETF,
|
||||
RETF: Future<Output = anyhow::Result<RET>> + Send + 'static,
|
||||
F: Send + 'static,
|
||||
RET: Send + 'static,
|
||||
{
|
||||
promise::spawn::spawn_into_main_thread(async move { schedule_with_lua(func).await }).await
|
||||
}
|
||||
|
||||
pub fn assign_lua_factory(make_lua_context: LuaFactory) {
|
||||
@ -206,13 +327,26 @@ impl ConfigInner {
|
||||
/// replace any captured error message.
|
||||
fn reload(&mut self) {
|
||||
match Config::load() {
|
||||
Ok((config, path)) => {
|
||||
Ok(LoadedConfig {
|
||||
config,
|
||||
file_name,
|
||||
lua,
|
||||
}) => {
|
||||
self.config = Arc::new(config);
|
||||
self.error.take();
|
||||
self.generation += 1;
|
||||
|
||||
// If we loaded a user config, publish this latest version of
|
||||
// the lua state to the LUA_PIPE. This allows a subsequent
|
||||
// call to `with_lua_config` to reference this lua context
|
||||
// even though we are (probably) resolving this from a background
|
||||
// reloading thread.
|
||||
if let Some(lua) = lua {
|
||||
LUA_PIPE.sender.try_send(lua).ok();
|
||||
}
|
||||
log::debug!("Reloaded configuration! generation={}", self.generation);
|
||||
if self.config.automatically_reload_config {
|
||||
if let Some(path) = path {
|
||||
if let Some(path) = file_name {
|
||||
self.watch_path(path);
|
||||
}
|
||||
}
|
||||
@ -677,8 +811,14 @@ impl Default for Config {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LoadedConfig {
|
||||
config: Config,
|
||||
file_name: Option<PathBuf>,
|
||||
lua: Option<mlua::Lua>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn load() -> Result<(Self, Option<PathBuf>), Error> {
|
||||
pub fn load() -> Result<LoadedConfig, Error> {
|
||||
// Note that the directories crate has methods for locating project
|
||||
// specific config directories, but only returns one of them, not
|
||||
// multiple. In addition, it spawns a lot of subprocesses,
|
||||
@ -744,10 +884,18 @@ impl Config {
|
||||
if let Some(dir) = p.parent() {
|
||||
std::env::set_var("WEZTERM_CONFIG_DIR", dir);
|
||||
}
|
||||
return Ok((cfg.compute_extra_defaults(Some(p)), Some(p.to_path_buf())));
|
||||
return Ok(LoadedConfig {
|
||||
config: cfg.compute_extra_defaults(Some(p)),
|
||||
file_name: Some(p.to_path_buf()),
|
||||
lua: Some(lua),
|
||||
});
|
||||
}
|
||||
|
||||
Ok((Self::default().compute_extra_defaults(None), None))
|
||||
Ok(LoadedConfig {
|
||||
config: Self::default().compute_extra_defaults(None),
|
||||
file_name: None,
|
||||
lua: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn default_config() -> Self {
|
||||
|
@ -54,6 +54,7 @@ fn main() {
|
||||
|
||||
fn run() -> anyhow::Result<()> {
|
||||
//stats::Stats::init()?;
|
||||
config::designate_this_as_the_main_thread();
|
||||
let _saver = umask::UmaskSaver::new();
|
||||
|
||||
let opts = Opt::from_args();
|
||||
|
@ -1872,16 +1872,41 @@ impl TermWindow {
|
||||
}
|
||||
OpenLinkAtMouseCursor => {
|
||||
// They clicked on a link, so let's open it!
|
||||
// Ensure that we spawn the `open` call outside of the context
|
||||
// We need to ensure that we spawn the `open` call outside of the context
|
||||
// of our window loop; on Windows it can cause a panic due to
|
||||
// triggering our WndProc recursively.
|
||||
// We get that assurance for free as part of the async dispatch that we
|
||||
// perform below; here we allow the user to define an `open-uri` event
|
||||
// handler that can bypass the normal `open::that` functionality.
|
||||
if let Some(link) = self.current_highlight.as_ref().cloned() {
|
||||
promise::spawn::spawn(async move {
|
||||
log::error!("clicking {}", link.uri());
|
||||
if let Err(err) = open::that(link.uri()) {
|
||||
log::error!("failed to open {}: {:?}", link.uri(), err);
|
||||
async fn open_uri(
|
||||
lua: Option<Rc<mlua::Lua>>,
|
||||
link: String,
|
||||
) -> anyhow::Result<()> {
|
||||
let default_click = match lua {
|
||||
Some(lua) => {
|
||||
let args = lua.pack_multi(link.clone())?;
|
||||
config::lua::emit_event(&lua, ("open-uri".to_string(), args))
|
||||
.await
|
||||
.map_err(|e| {
|
||||
log::error!("while processing open-uri event: {:#}", e);
|
||||
e
|
||||
})?
|
||||
}
|
||||
None => true,
|
||||
};
|
||||
if default_click {
|
||||
log::error!("clicking {}", link);
|
||||
if let Err(err) = open::that(&link) {
|
||||
log::error!("failed to open {}: {:?}", link, err);
|
||||
}
|
||||
}
|
||||
})
|
||||
Ok(())
|
||||
}
|
||||
|
||||
promise::spawn::spawn(config::with_lua_config_on_main_thread(move |lua| {
|
||||
open_uri(lua, link.uri().to_string())
|
||||
}))
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
@ -594,6 +594,7 @@ fn terminate_with_error(err: anyhow::Error) -> ! {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
config::designate_this_as_the_main_thread();
|
||||
config::assign_error_callback(crate::connui::show_configuration_error_message);
|
||||
notify_on_panic();
|
||||
if let Err(e) = run() {
|
||||
|
Loading…
Reference in New Issue
Block a user