Something went wrong. Try again.
WezTerm with vertical (side) tab bar — Nix flake; based on #3537 / PR #7574
Something went wrong. Try again.
8.6 kB · 275 lines
Rust
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276use 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, plugin_dir: PathBuf,}
/// Given a URL, generate a string that can be used as a directory name./// The returned name must be a single valid filesystem componentfn 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_str("sDs"); } '-' | '_' => dir.push(c), c if c.is_alphanumeric() => dir.push(c), c => dir.push_str(&format!("u{}", c as u32)), } } if dir.ends_with("sZs") { dir.truncate(dir.len() - 3); } 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}"); }
let plugin_dir = RepoSpec::plugins_dir().join(&component);
Ok(Self { url, component, plugin_dir, }) }
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 plugin_dir = RepoSpec::plugins_dir().join(&component);
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, plugin_dir, }); } anyhow::bail!("Unable to create a complete RepoSpec for repo at {path:?}"); }
fn plugins_dir() -> PathBuf { config::DATA_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)); 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); Repository::clone_recurse(&self.url, target_dir.path())?; let target_dir = target_dir.keep(); 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"), ( "githubsDscom/wezterm/wezterm-plugins", "githubsDscomsZsweztermsZswezterm-plugins", ), ("localhost:8080/repo", "localhostsCs8080sZsrepo"), ] { let result = compute_repo_dir(input); assert_eq!(&result, expect, "for input {input}"); } }}