diff --git a/.vimrc b/.vimrc index 3ef1e99..8884e84 100644 --- a/.vimrc +++ b/.vimrc @@ -33,6 +33,13 @@ inoremap nnoremap Q :q! vnoremap v V +" load plugins from ~/.vim/plugged/ +for s:dir in glob('~/.vim/plugged/*', 0, 1) + if isdirectory(s:dir) + exe 'set rtp+=' . s:dir + endif +endfor + syntax on filetype plugin indent on au BufReadPost * if line("'\"") > 0 && line("'\"") <= line("$") | exe "normal! g`\"" | endif diff --git a/rust/ais/src/main.rs b/rust/ais/src/main.rs index bd7a55b..49c0b51 100644 --- a/rust/ais/src/main.rs +++ b/rust/ais/src/main.rs @@ -1,6 +1,6 @@ // ais - ai selector -// unified fuzzy finder: directories, files, history -// usage: ais [cd|file|hist] or pipe: echo "a\nb" | ais +// unified fuzzy finder + plugin manager +// usage: ais [cd|file|hist|plugin] or pipe: echo "a\nb" | ais use std::collections::HashSet; use std::env; @@ -11,6 +11,16 @@ fn main() { let args: Vec = env::args().collect(); let mode = args.get(1).map(|s| s.as_str()).unwrap_or("cd"); + match mode { + "plugin" | "p" => { + let sub = args.get(2).map(|s| s.as_str()).unwrap_or("ls"); + let rest: Vec<&str> = args.iter().skip(3).map(|s| s.as_str()).collect(); + plugin_cmd(sub, &rest); + return; + } + _ => {} + } + let items = if !is_tty_stdin() { read_stdin_lines() } else { @@ -19,7 +29,7 @@ fn main() { "file" | "f" => get_files(), "hist" | "h" => get_history(), _ => { - eprintln!("usage: ais [cd|file|hist]"); + eprintln!("usage: ais [cd|file|hist|plugin]"); return; } } @@ -34,6 +44,125 @@ fn main() { } } +// --- plugin manager --- + +fn plugin_dir() -> String { + let home = env::var("HOME").unwrap_or_default(); + format!("{}/.vim/plugged", home) +} + +fn plugin_cmd(sub: &str, args: &[&str]) { + match sub { + "add" | "a" => { + if args.is_empty() { + eprintln!("usage: ais plugin add "); + return; + } + plugin_add(args[0]); + } + "rm" | "r" => plugin_rm(args.first().copied()), + "ls" | "l" | "" => plugin_ls(), + _ => eprintln!("usage: ais plugin [add|ls|rm]"), + } +} + +fn plugin_add(name: &str) { + let dir = plugin_dir(); + std::fs::create_dir_all(&dir).ok(); + + // accept "user/repo" or full github url + let (url, repo_name) = if name.contains("://") { + let repo = name.rsplit('/').next().unwrap_or(name); + let repo = repo.strip_suffix(".git").unwrap_or(repo); + (name.to_string(), repo.to_string()) + } else { + let repo = name.split('/').last().unwrap_or(name); + (format!("https://github.com/{}.git", name), repo.to_string()) + }; + + let dest = format!("{}/{}", dir, repo_name); + if std::path::Path::new(&dest).exists() { + eprintln!("{} already installed", repo_name); + return; + } + + eprintln!("installing {} ...", repo_name); + let status = Command::new("git") + .args(["clone", "--depth", "1", &url, &dest]) + .status(); + + match status { + Ok(s) if s.success() => eprintln!("ok: {}", repo_name), + _ => eprintln!("failed: {}", url), + } +} + +fn plugin_ls() { + let dir = plugin_dir(); + let entries = match std::fs::read_dir(&dir) { + Ok(e) => e, + Err(_) => { + eprintln!("no plugins"); + return; + } + }; + + for entry in entries.flatten() { + if entry.file_type().map(|t| t.is_dir()).unwrap_or(false) { + let name = entry.file_name().to_string_lossy().to_string(); + if !name.starts_with('.') { + println!("{}", name); + } + } + } +} + +fn plugin_rm(name: Option<&str>) { + let dir = plugin_dir(); + + let target = if let Some(n) = name { + n.to_string() + } else { + // fuzzy select from installed plugins + let entries = match std::fs::read_dir(&dir) { + Ok(e) => e, + Err(_) => { + eprintln!("no plugins"); + return; + } + }; + let plugins: Vec = entries + .flatten() + .filter(|e| e.file_type().map(|t| t.is_dir()).unwrap_or(false)) + .map(|e| e.file_name().to_string_lossy().to_string()) + .filter(|n| !n.starts_with('.')) + .collect(); + + if plugins.is_empty() { + eprintln!("no plugins"); + return; + } + + match fuzzy_select(&plugins, "rm plugin") { + Some(s) => s, + None => return, + } + }; + + let path = format!("{}/{}", dir, target); + if !std::path::Path::new(&path).exists() { + eprintln!("{} not found", target); + return; + } + + match std::fs::remove_dir_all(&path) { + Ok(_) => eprintln!("removed: {}", target), + Err(e) => eprintln!("failed: {}", e), + } +} + +// --- selectors --- + fn is_tty_stdin() -> bool { extern "C" { fn isatty(fd: i32) -> i32; @@ -123,6 +252,8 @@ fn read_stdin_lines() -> Vec { .collect() } +// --- fuzzy select TUI --- + fn fuzzy_match(item: &str, query: &str) -> bool { if query.is_empty() { return true;