use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::UnixStream;
mod daemon;
mod parser;
#[derive(Parser)]
#[command(name = "autocomplete-rs")]
#[command(about = "Fast, universal terminal autocomplete", long_about = None)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Daemon {
#[arg(short, long, default_value = "/tmp/autocomplete-rs.sock")]
socket: String,
},
Stop {
#[arg(short, long, default_value = "/tmp/autocomplete-rs.sock")]
socket: String,
},
Status {
#[arg(short, long, default_value = "/tmp/autocomplete-rs.sock")]
socket: String,
},
Complete {
buffer: String,
#[arg(short, long)]
cursor: usize,
#[arg(short, long, default_value = "/tmp/autocomplete-rs.sock")]
socket: String,
},
Install {
shell: String,
},
}
#[tokio::main]
async fn main() -> Result<()> {
if std::env::args().any(|arg| arg == "daemon") {
tracing_subscriber::fmt::init();
}
let cli = Cli::parse();
match cli.command {
Commands::Daemon { socket } => {
tracing::info!("Starting autocomplete daemon on {}", socket);
daemon::start(&socket).await?;
}
Commands::Stop { socket } => {
stop_daemon(&socket).await?;
}
Commands::Status { socket } => {
status_command(&socket).await?;
}
Commands::Complete {
buffer,
cursor,
socket,
} => {
complete_command(&buffer, cursor, &socket).await?;
}
Commands::Install { shell } => {
install_command(&shell)?;
}
}
Ok(())
}
async fn complete_command(buffer: &str, cursor: usize, socket_path: &str) -> Result<()> {
let stream = UnixStream::connect(socket_path)
.await
.context("Failed to connect to daemon. Is it running?")?;
let (reader, mut writer) = stream.into_split();
let mut reader = BufReader::new(reader);
let request = daemon::CompletionRequest {
buffer: buffer.to_string(),
cursor,
version: 1,
};
let request_json = serde_json::to_string(&request)?;
writer.write_all(request_json.as_bytes()).await?;
writer.write_all(b"\n").await?;
writer.flush().await?;
let mut response_line = String::new();
reader.read_line(&mut response_line).await?;
print!("{}", response_line.trim());
Ok(())
}
async fn stop_daemon(socket_path: &str) -> Result<()> {
use std::path::Path;
if !Path::new(socket_path).exists() {
println!("Daemon is not running (socket not found)");
return Ok(());
}
match UnixStream::connect(socket_path).await {
Ok(_stream) => {
std::fs::remove_file(socket_path)?;
println!("Daemon stopped");
}
Err(_) => {
std::fs::remove_file(socket_path)?;
println!("Removed stale socket (daemon was not running)");
}
}
Ok(())
}
async fn status_command(socket_path: &str) -> Result<()> {
use std::path::Path;
if !Path::new(socket_path).exists() {
println!("Daemon is not running (socket not found)");
return Ok(());
}
match UnixStream::connect(socket_path).await {
Ok(_stream) => {
println!("Daemon is running on {}", socket_path);
}
Err(_) => {
println!("Socket exists but daemon is not responding (stale socket)");
}
}
Ok(())
}
fn install_command(shell: &str) -> Result<()> {
match shell {
"zsh" => {
println!("To install autocomplete-rs for zsh, add this to your ~/.zshrc:");
println!();
println!("# autocomplete-rs");
println!("source <(autocomplete-rs shell-init zsh)");
println!();
println!("Or manually source the integration script:");
println!("source /path/to/autocomplete-rs/shell-integration/zsh.zsh");
}
_ => {
anyhow::bail!(
"Unsupported shell: {}. Currently only 'zsh' is supported.",
shell
);
}
}
Ok(())
}