1
1
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:
Wez Furlong 2020-10-07 18:19:14 -07:00
parent 882f47f4ad
commit 9397f2a2db
6 changed files with 188 additions and 11 deletions

1
Cargo.lock generated
View File

@ -696,6 +696,7 @@ dependencies = [
"notify",
"portable-pty",
"pretty_env_logger",
"promise",
"serde",
"smol",
"termwiz",

View File

@ -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"] }

View File

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

View File

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

View File

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

View File

@ -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() {