use anyhow::Result;
use rustyline::completion::{Completer, FilenameCompleter};
use rustyline::error::ReadlineError;
use rustyline::highlight::Highlighter;
use rustyline::hint::Hinter;
use rustyline::validate::Validator;
use rustyline::{CompletionType, Config, Context, Editor, Helper};
use std::path::PathBuf;
#[derive(Default)]
struct KeloraHelper {
completer: FilenameCompleter,
}
impl Completer for KeloraHelper {
type Candidate = <FilenameCompleter as Completer>::Candidate;
fn complete(
&self,
line: &str,
pos: usize,
ctx: &Context<'_>,
) -> rustyline::Result<(usize, Vec<Self::Candidate>)> {
self.completer.complete(line, pos, ctx)
}
}
impl Hinter for KeloraHelper {
type Hint = String;
}
impl Highlighter for KeloraHelper {}
impl Validator for KeloraHelper {}
impl Helper for KeloraHelper {}
pub fn run_interactive_mode() -> Result<()> {
let config = Config::builder()
.completion_type(CompletionType::List)
.build();
let helper = KeloraHelper::default();
let mut rl = Editor::with_config(config)?;
rl.set_helper(Some(helper));
let history_path = get_history_path();
if let Some(ref path) = history_path {
let _ = rl.load_history(path);
}
println!("Kelora Interactive Mode — :quit to exit, :help for help\n");
loop {
let readline = rl.readline("kelora> ");
match readline {
Ok(line) => {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
let _ = rl.add_history_entry(trimmed);
if trimmed == ":exit" || trimmed == ":quit" || trimmed == ":q" {
break;
}
if trimmed == ":help" {
let eof_key = if cfg!(windows) { "Ctrl-Z" } else { "Ctrl-D" };
println!("Interactive mode - enter kelora commands without 'kelora' prefix");
println!();
println!(" TAB Complete files/directories");
println!(" *.log Glob patterns auto-expand");
println!(" 'foo bar' Use quotes when args contain spaces");
println!(" --help See all kelora options");
println!();
println!(" Ctrl-C Cancel running command");
println!(" :quit Exit (or :q, :exit, {})", eof_key);
println!();
println!("Example: -j mylog.json --filter 'e.status >= 500'");
continue;
}
match parse_and_execute_command(trimmed) {
Ok(()) => {
}
Err(e) => {
eprintln!("Error: {}", e);
}
}
}
Err(ReadlineError::Interrupted) => {
continue;
}
Err(ReadlineError::Eof) => {
break;
}
Err(err) => {
eprintln!("Error reading line: {}", err);
break;
}
}
}
if let Some(ref path) = history_path {
let _ = rl.save_history(path);
}
Ok(())
}
fn parse_and_execute_command(line: &str) -> Result<()> {
let words = shell_words::split(line)?;
if words.is_empty() {
return Ok(());
}
let expanded_args = expand_globs(&words)?;
let mut args = vec!["kelora".to_string()];
args.extend(expanded_args);
execute_kelora_command(args)?;
Ok(())
}
fn expand_globs(args: &[String]) -> Result<Vec<String>> {
let mut result = Vec::new();
for arg in args {
if arg.contains('*') || arg.contains('?') || arg.contains('[') {
let mut matches: Vec<String> = glob::glob(arg)?
.filter_map(|path| path.ok())
.map(|path| path.to_string_lossy().to_string())
.collect();
if matches.is_empty() {
result.push(arg.clone());
} else {
matches.sort();
result.extend(matches);
}
} else {
result.push(arg.clone());
}
}
Ok(result)
}
fn execute_kelora_command(args: Vec<String>) -> Result<()> {
use std::process::Command;
let exe_path = std::env::current_exe()?;
let cmd_args = &args[1..];
let status = Command::new(&exe_path).args(cmd_args).status()?;
if !status.success() {
if let Some(code) = status.code() {
if code != 0 {
}
}
}
Ok(())
}
fn get_history_path() -> Option<PathBuf> {
dirs::config_dir().and_then(|mut path| {
path.push("kelora");
if let Err(_e) = std::fs::create_dir_all(&path) {
return None;
}
path.push("interactive_history.txt");
Some(path)
})
}