use std::sync::Arc;
use rustyline::completion::{Completer, Pair};
use rustyline::highlight::Highlighter;
use rustyline::hint::Hinter;
use rustyline::validate::Validator;
use rustyline::{Context, Helper};
use crate::fs::FileSystem;
use crate::storage::VaultBackend;
use crate::vault::VaultManager;
pub const COMMANDS: &[&str] = &[
"vault", "ls", "cat", "checkpoint", "write", "mkdir", "rm", "cp", "mv", "tree", "pwd", "log", "checkout",
"revert", "diff", "search", "grep", "find", "tag", "untag", "meta", "import", "export", "exec",
"stats", "prune", "gc", "compact", "maintain", "quota", "audit", "snapshot",
#[cfg(feature = "fuse")]
"mount",
#[cfg(feature = "fuse")]
"unmount",
#[cfg(feature = "fuse")]
"proxy",
"exit", "quit", "help", "clear",
];
pub const SUBCOMMANDS: &[(&str, &[&str])] = &[
("vault", &["create", "list", "use", "delete", "info", "fork"]),
("checkpoint", &["save", "list", "restore", "delete", "info"]),
("snapshot", &["save", "list", "restore", "delete", "info"]),
("quota", &["set", "clear"]),
("audit", &["clear"]),
("tag", &["--list", "--create", "--delete", "--rename"]),
#[cfg(feature = "fuse")]
("proxy", &["exec"]),
];
pub struct ShellHelper {
backend: Option<Arc<VaultBackend>>,
}
impl ShellHelper {
pub fn new() -> Self {
Self { backend: None }
}
pub fn update_backend(&mut self) {
if let Ok(manager) = VaultManager::new() {
if let Ok(backend) = manager.open_current() {
self.backend = Some(backend);
return;
}
}
self.backend = None;
}
fn complete_command(&self, partial: &str) -> Vec<Pair> {
COMMANDS
.iter()
.filter(|cmd| cmd.starts_with(partial))
.map(|cmd| Pair {
display: cmd.to_string(),
replacement: cmd.to_string(),
})
.collect()
}
fn complete_subcommand(&self, cmd: &str, partial: &str) -> Vec<Pair> {
for (command, subs) in SUBCOMMANDS {
if *command == cmd {
return subs
.iter()
.filter(|sub| sub.starts_with(partial))
.map(|sub| Pair {
display: sub.to_string(),
replacement: sub.to_string(),
})
.collect();
}
}
Vec::new()
}
fn complete_path(&self, partial: &str) -> Vec<Pair> {
let Some(backend) = &self.backend else {
return Vec::new();
};
let fs = FileSystem::new(backend.clone());
let (parent, prefix) = if partial.is_empty() || partial == "/" {
("/", "")
} else if partial.ends_with('/') {
(partial, "")
} else {
match partial.rfind('/') {
Some(pos) => {
let parent = if pos == 0 { "/" } else { &partial[..pos] };
let prefix = &partial[pos + 1..];
(parent, prefix)
}
None => ("/", partial),
}
};
let entries = match fs.list_dir(parent) {
Ok(entries) => entries,
Err(_) => return Vec::new(),
};
entries
.iter()
.filter(|entry| entry.name.starts_with(prefix))
.map(|entry| {
let path = if parent == "/" {
format!("/{}", entry.name)
} else {
format!("{}/{}", parent, entry.name)
};
let display = if entry.file_type.is_dir() {
format!("{}/", entry.name)
} else {
entry.name.clone()
};
let replacement = if entry.file_type.is_dir() {
format!("{}/", path)
} else {
path
};
Pair {
display,
replacement,
}
})
.collect()
}
}
impl Completer for ShellHelper {
type Candidate = Pair;
fn complete(
&self,
line: &str,
pos: usize,
_ctx: &Context<'_>,
) -> rustyline::Result<(usize, Vec<Pair>)> {
let line = &line[..pos];
let words: Vec<&str> = line.split_whitespace().collect();
match words.len() {
0 => {
Ok((0, self.complete_command("")))
}
1 => {
if line.ends_with(' ') {
let cmd = words[0];
let subs = self.complete_subcommand(cmd, "");
if !subs.is_empty() {
Ok((pos, subs))
} else {
Ok((pos, self.complete_path("")))
}
} else {
Ok((0, self.complete_command(words[0])))
}
}
_ => {
let cmd = words[0];
let last = words.last().unwrap();
if line.ends_with(' ') {
if words.len() == 1 {
let subs = self.complete_subcommand(cmd, "");
if !subs.is_empty() {
return Ok((pos, subs));
}
}
Ok((pos, self.complete_path("")))
} else {
let start = line.rfind(' ').map(|i| i + 1).unwrap_or(0);
if words.len() == 2 {
let subs = self.complete_subcommand(cmd, last);
if !subs.is_empty() {
return Ok((start, subs));
}
}
if last.starts_with('/') || last.contains('/') {
Ok((start, self.complete_path(last)))
} else {
let paths = self.complete_path(last);
Ok((start, paths))
}
}
}
}
}
}
impl Hinter for ShellHelper {
type Hint = String;
fn hint(&self, _line: &str, _pos: usize, _ctx: &Context<'_>) -> Option<String> {
None
}
}
impl Highlighter for ShellHelper {}
impl Validator for ShellHelper {}
impl Helper for ShellHelper {}