mirror of
https://github.com/wez/wezterm.git
synced 2024-09-11 22:37:11 +03:00
Add wezterm.plugin module, allows loading modules from git
Brief usage notes here: ```lua local wezterm = require 'wezterm' local a_plugin = wezterm.plugin.require "https://github.com/owner/repo" local config = wezterm.config_builder() a_plugin.apply_to_config(config) return config ``` The referenced repo is expected to have a `plugin/init.lua` file, and by convention, return a module that exports an `apply_to_config` function that accepts at least a config builder parameter, but may pass other parameters, or a lua table with a `config` field that maps to a config build parameter. `wezterm.plugin.require` will clone the repo if it doesn't already exist and store it in the runtime dir under `plugins/NAME` where `NAME` is derived from the repo URL. Once cloned, the repo is NOT automatically updated. Only HTTP (or local filesystem) repos are allowed for the git URL; we cannot currently use ssh for this due to conflicting version requirements that I'll take a look at later. `wezterm.plugin.require` will then perform `require "NAME"`, and since the default `package.path` now includes the appropriate location from the runtime dir, the module should load. Two other functions are available: `wezterm.plugin.list()` will list the plugin repos. `wezterm.plugin.update_all()` will attempt to fast-forward or `pull --rebase` each of the repos it finds. It doesn't currently do anything proactive to reload the configuration afterwards; the user will need to do that themselves.
This commit is contained in:
parent
df12dd9d00
commit
e4ae8a844d
25
Cargo.lock
generated
25
Cargo.lock
generated
@ -1343,6 +1343,7 @@ dependencies = [
|
||||
"logging",
|
||||
"mux-lua",
|
||||
"objc",
|
||||
"plugin",
|
||||
"procinfo-funcs",
|
||||
"share-data",
|
||||
"spawn-funcs",
|
||||
@ -1775,14 +1776,16 @@ checksum = "221996f774192f0f718773def8201c4ae31f02616a54ccfc2d358bb0e5cefdec"
|
||||
|
||||
[[package]]
|
||||
name = "git2"
|
||||
version = "0.14.4"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0155506aab710a86160ddb504a480d2964d7ab5b9e62419be69e0032bc5931c"
|
||||
checksum = "ccf7f68c2995f392c49fffb4f95ae2c873297830eb25c6bc4c114ce8f4562acc"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"libc",
|
||||
"libgit2-sys",
|
||||
"log",
|
||||
"openssl-probe",
|
||||
"openssl-sys",
|
||||
"url",
|
||||
]
|
||||
|
||||
@ -2475,13 +2478,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libgit2-sys"
|
||||
version = "0.13.5+1.4.5"
|
||||
version = "0.14.2+1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51e5ea06c26926f1002dd553fded6cfcdc9784c1f60feeb58368b4d9b07b6dba"
|
||||
checksum = "7f3d95f6b51075fe9810a7ae22c7095f12b98005ab364d8544797a825ce946a4"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"libz-sys",
|
||||
"openssl-sys",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
@ -3644,6 +3648,19 @@ dependencies = [
|
||||
"plotters-backend",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "plugin"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"config",
|
||||
"git2",
|
||||
"log",
|
||||
"luahelper",
|
||||
"tempfile",
|
||||
"wezterm-dynamic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.17.7"
|
||||
|
@ -11,7 +11,7 @@ build = "build.rs"
|
||||
env_logger = "0.10"
|
||||
|
||||
[build-dependencies]
|
||||
git2 = { version = "0.14", default-features = false }
|
||||
git2 = { version = "0.16", default-features = false }
|
||||
|
||||
[features]
|
||||
distro-defaults = []
|
||||
|
@ -248,6 +248,11 @@ pub fn make_lua_context(config_file: &Path) -> anyhow::Result<Lua> {
|
||||
|
||||
prefix_path(&mut path_array, &crate::HOME_DIR.join(".wezterm"));
|
||||
prefix_path(&mut path_array, &crate::CONFIG_DIR);
|
||||
path_array.insert(
|
||||
2,
|
||||
format!("{}/plugins/?/plugin/init.lua", crate::RUNTIME_DIR.display()),
|
||||
);
|
||||
|
||||
if let Ok(exe) = std::env::current_exe() {
|
||||
if let Some(path) = exe.parent() {
|
||||
wezterm_mod.set(
|
||||
|
@ -24,6 +24,7 @@ mux-lua = { path = "../lua-api-crates/mux" }
|
||||
procinfo-funcs = { path = "../lua-api-crates/procinfo-funcs" }
|
||||
filesystem = { path = "../lua-api-crates/filesystem" }
|
||||
json = { path = "../lua-api-crates/json" }
|
||||
plugin = { path = "../lua-api-crates/plugin" }
|
||||
share-data = { path = "../lua-api-crates/share-data" }
|
||||
ssh-funcs = { path = "../lua-api-crates/ssh-funcs" }
|
||||
spawn-funcs = { path = "../lua-api-crates/spawn-funcs" }
|
||||
|
@ -168,6 +168,7 @@ fn register_lua_modules() {
|
||||
procinfo_funcs::register,
|
||||
filesystem::register,
|
||||
json::register,
|
||||
plugin::register,
|
||||
ssh_funcs::register,
|
||||
spawn_funcs::register,
|
||||
share_data::register,
|
||||
|
15
lua-api-crates/plugin/Cargo.toml
Normal file
15
lua-api-crates/plugin/Cargo.toml
Normal file
@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "plugin"
|
||||
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" }
|
||||
git2 = { version = "0.16", default-features = false, features = ["https"] }
|
||||
log = "0.4"
|
||||
luahelper = { path = "../../luahelper" }
|
||||
tempfile = "3.3"
|
||||
wezterm-dynamic = { path = "../../wezterm-dynamic" }
|
256
lua-api-crates/plugin/src/lib.rs
Normal file
256
lua-api-crates/plugin/src/lib.rs
Normal file
@ -0,0 +1,256 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use config::lua::get_or_create_sub_module;
|
||||
use config::lua::mlua::{self, Lua, Value};
|
||||
use git2::build::CheckoutBuilder;
|
||||
use git2::{Remote, Repository};
|
||||
use luahelper::to_lua;
|
||||
use std::path::PathBuf;
|
||||
use tempfile::TempDir;
|
||||
use wezterm_dynamic::{FromDynamic, ToDynamic};
|
||||
|
||||
#[derive(FromDynamic, ToDynamic, Debug)]
|
||||
struct RepoSpec {
|
||||
url: String,
|
||||
component: String,
|
||||
}
|
||||
|
||||
/// Given a URL, generate a string that can be used as a directory name.
|
||||
/// The returned name must be a single valid filesystem component
|
||||
fn compute_repo_dir(url: &str) -> String {
|
||||
let mut dir = String::new();
|
||||
for c in url.chars() {
|
||||
match c {
|
||||
'/' | '\\' => {
|
||||
dir.push_str("sZs");
|
||||
}
|
||||
':' => {
|
||||
dir.push_str("sCs");
|
||||
}
|
||||
'.' | '-' | '_' => dir.push(c),
|
||||
c if c.is_alphanumeric() => dir.push(c),
|
||||
c => dir.push_str(&format!("u{}", c as u32)),
|
||||
}
|
||||
}
|
||||
dir
|
||||
}
|
||||
|
||||
fn get_remote(repo: &Repository) -> anyhow::Result<Option<Remote>> {
|
||||
let remotes = repo.remotes()?;
|
||||
for remote in remotes.iter() {
|
||||
if let Some(name) = remote {
|
||||
let remote = repo.find_remote(name)?;
|
||||
return Ok(Some(remote));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
impl RepoSpec {
|
||||
fn parse(url: String) -> anyhow::Result<Self> {
|
||||
let component = compute_repo_dir(&url);
|
||||
if component.starts_with('.') {
|
||||
anyhow::bail!("invalid repo spec {url}");
|
||||
}
|
||||
|
||||
Ok(Self { url, component })
|
||||
}
|
||||
|
||||
fn load_from_dir(path: PathBuf) -> anyhow::Result<Self> {
|
||||
let component = path
|
||||
.file_name()
|
||||
.ok_or_else(|| anyhow!("missing file name!?"))?
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow!("{path:?} isn't unicode"))?
|
||||
.to_string();
|
||||
|
||||
let repo = Repository::open(&path)?;
|
||||
let remote = get_remote(&repo)?.ok_or_else(|| anyhow!("no remotes!?"))?;
|
||||
let url = remote.url();
|
||||
if let Some(url) = url {
|
||||
let url = url.to_string();
|
||||
return Ok(Self { component, url });
|
||||
}
|
||||
anyhow::bail!("Unable to create a complete RepoSpec for repo at {path:?}");
|
||||
}
|
||||
|
||||
fn plugins_dir() -> PathBuf {
|
||||
config::RUNTIME_DIR.join("plugins")
|
||||
}
|
||||
|
||||
fn checkout_path(&self) -> PathBuf {
|
||||
Self::plugins_dir().join(&self.component)
|
||||
}
|
||||
|
||||
fn is_checked_out(&self) -> bool {
|
||||
self.checkout_path().exists()
|
||||
}
|
||||
|
||||
fn update(&self) -> anyhow::Result<()> {
|
||||
let path = self.checkout_path();
|
||||
let repo = Repository::open(&path)?;
|
||||
let mut remote = get_remote(&repo)?.ok_or_else(|| anyhow!("no remotes!?"))?;
|
||||
remote.connect(git2::Direction::Fetch).context("connect")?;
|
||||
let branch = remote
|
||||
.default_branch()
|
||||
.context("get default branch")?
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow!("default branch is not utf8"))?
|
||||
.to_string();
|
||||
|
||||
remote.fetch(&[branch], None, None).context("fetch")?;
|
||||
let mut merge_info = None;
|
||||
repo.fetchhead_foreach(|refname, _remote_url, target_oid, was_merge| {
|
||||
if was_merge {
|
||||
merge_info.replace((refname.to_string(), target_oid.clone()));
|
||||
return true;
|
||||
}
|
||||
false
|
||||
})
|
||||
.context("fetchhead_foreach")?;
|
||||
|
||||
let (refname, target_oid) = merge_info.ok_or_else(|| anyhow!("No merge info!?"))?;
|
||||
let commit = repo
|
||||
.find_annotated_commit(target_oid)
|
||||
.context("find_annotated_commit")?;
|
||||
|
||||
let (analysis, _preference) = repo.merge_analysis(&[&commit]).context("merge_analysis")?;
|
||||
if analysis.is_up_to_date() {
|
||||
log::debug!("{} is up to date!", self.component);
|
||||
return Ok(());
|
||||
}
|
||||
if analysis.is_fast_forward() {
|
||||
log::debug!("{} can fast forward!", self.component);
|
||||
let mut reference = repo.find_reference(&refname).context("find_reference")?;
|
||||
reference
|
||||
.set_target(target_oid, "fast forward")
|
||||
.context("set_target")?;
|
||||
repo.checkout_head(Some(CheckoutBuilder::new().force()))
|
||||
.context("checkout_head")?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
log::debug!("{} will merge", self.component);
|
||||
repo.merge(&[&commit], None, Some(CheckoutBuilder::new().safe()))
|
||||
.context("merge")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_out(&self) -> anyhow::Result<()> {
|
||||
let plugins_dir = Self::plugins_dir();
|
||||
std::fs::create_dir_all(&plugins_dir)?;
|
||||
let target_dir = TempDir::new_in(&plugins_dir)?;
|
||||
log::debug!("Cloning {} into temporary dir {target_dir:?}", self.url);
|
||||
let _repo = Repository::clone_recurse(&self.url, target_dir.path())?;
|
||||
let target_dir = target_dir.into_path();
|
||||
let checkout_path = self.checkout_path();
|
||||
match std::fs::rename(&target_dir, &checkout_path) {
|
||||
Ok(_) => {
|
||||
log::info!("Cloned {} into {checkout_path:?}", self.url);
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(
|
||||
"Failed to rename {target_dir:?} -> {:?}, removing temporary dir",
|
||||
self.checkout_path()
|
||||
);
|
||||
if let Err(err) = std::fs::remove_dir_all(&target_dir) {
|
||||
log::error!(
|
||||
"Failed to remove {target_dir:?}: {err:#}, \
|
||||
you will need to remove it manually"
|
||||
);
|
||||
}
|
||||
Err(err.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn require_plugin(lua: &Lua, url: String) -> anyhow::Result<Value> {
|
||||
let spec = RepoSpec::parse(url)?;
|
||||
|
||||
if !spec.is_checked_out() {
|
||||
spec.check_out()?;
|
||||
}
|
||||
|
||||
let require: mlua::Function = lua.globals().get("require")?;
|
||||
match require.call::<_, Value>(spec.component.to_string()) {
|
||||
Ok(value) => Ok(value),
|
||||
Err(err) => {
|
||||
log::error!(
|
||||
"Failed to require {} which is stored in {:?}: {err:#}",
|
||||
spec.component,
|
||||
spec.checkout_path()
|
||||
);
|
||||
Err(err.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn list_plugins() -> anyhow::Result<Vec<RepoSpec>> {
|
||||
let mut plugins = vec![];
|
||||
|
||||
let plugins_dir = RepoSpec::plugins_dir();
|
||||
std::fs::create_dir_all(&plugins_dir)?;
|
||||
|
||||
for entry in plugins_dir.read_dir()? {
|
||||
let entry = entry?;
|
||||
if entry.path().is_dir() {
|
||||
plugins.push(RepoSpec::load_from_dir(entry.path())?);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(plugins)
|
||||
}
|
||||
|
||||
pub fn register(lua: &Lua) -> anyhow::Result<()> {
|
||||
let plugin_mod = get_or_create_sub_module(lua, "plugin")?;
|
||||
plugin_mod.set(
|
||||
"require",
|
||||
lua.create_function(|lua: &Lua, repo_spec: String| {
|
||||
require_plugin(lua, repo_spec).map_err(|e| mlua::Error::external(format!("{e:#}")))
|
||||
})?,
|
||||
)?;
|
||||
|
||||
plugin_mod.set(
|
||||
"list",
|
||||
lua.create_function(|lua, _: ()| {
|
||||
let plugins = list_plugins().map_err(|e| mlua::Error::external(format!("{e:#}")))?;
|
||||
to_lua(lua, plugins)
|
||||
})?,
|
||||
)?;
|
||||
|
||||
plugin_mod.set(
|
||||
"update_all",
|
||||
lua.create_function(|_, _: ()| {
|
||||
let plugins = list_plugins().map_err(|e| mlua::Error::external(format!("{e:#}")))?;
|
||||
for p in plugins {
|
||||
match p.update() {
|
||||
Ok(_) => log::info!("Updated {p:?}"),
|
||||
Err(err) => log::error!("Failed to update {p:?}: {err:#}"),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_compute_repo_dir() {
|
||||
for (input, expect) in &[
|
||||
("foo", "foo"),
|
||||
(
|
||||
"github.com/wez/wezterm-plugins",
|
||||
"github.comsZswezsZswezterm-plugins",
|
||||
),
|
||||
("localhost:8080/repo", "localhostsCs8080sZsrepo"),
|
||||
] {
|
||||
let result = compute_repo_dir(input);
|
||||
assert_eq!(&result, expect, "for input {input}");
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user