-
-
Notifications
You must be signed in to change notification settings - Fork 840
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
Showing
7 changed files
with
300 additions
and
5 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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}"); | ||
} | ||
} | ||
} |
e4ae8a8
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dude, this is super cool! I have one note, a use case which maybe you haven't thought of; my smart-splits.nvim Neovim plugin requires some custom Wezterm config for the integration with Wezterm's mux. I was thinking, now that Wezterm has a plugin system, I could bundle those configs into a Wezterm plugin so users don't have to copy much code into their Wezterm config.
However, looking at your commit message, I see this:
This means that if I put my Wezterm plugin in the same repo, that file will also be loaded by Neovim itself, but Neovim doesn't actually need it.
This isn't a huge deal, it will maybe just add a few ms to initial load, or alternatively I can put it in a separate repo, but just wanted to check, since I don't see docs yet, is this WIP? Would you be open to changing that
plugin/init.lua
convention to something else that doesn't conflict with Neovim's runtimepath?One other question, is there, currently or planned for the future, a way to provide configuration of the plugin itself? For example, I'd like to have a plugin that binds keymaps to special actions, but I'd like the user to be able to specify the keys.
e4ae8a8
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mrjones2014 I noticed that also when looking at smart-splits.nvim. Maybe the snippet could just be updated to use the "own modules" example. I know plugin is cleaner. But a separate file would look be better until the plugin support is extended.
https://wezfurlong.org/wezterm/config/files.html?h=helpers.lua#making-your-own-lua-modules
e4ae8a8
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you file an issue for this? I can't effectively track or triage comments on commits!