use clap::{Parser, Subcommand};
use serde::Serialize;
use std::collections::HashSet;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use zellij_rs::options::ZellijOptions;
use zesh::clone::CloneService;
use zesh::connection::ConnectService;
use zesh::fs::RealFs;
use zesh_git::RealGit;
use zellij_rs::{ZellijClient, ZellijOperations};
use zox_rs::{ZoxideClient, ZoxideOperations};
#[derive(Parser)]
#[clap(version, about, long_about = None)]
#[clap(propagate_version = true)]
struct Cli {
#[clap(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
#[clap(visible_alias = "l")]
List {
#[clap(short = 'Z', long)]
zesh: bool,
#[clap(short, long)]
zoxide: bool,
#[clap(short, long)]
json: bool,
#[clap(short = 'H', long)]
hide_attached: bool,
#[clap(short = 'd', long)]
hide_duplicates: bool,
},
#[clap(visible_alias = "cn")]
Connect {
name: String,
#[clap(flatten)]
zellij_options: ZellijOptions,
},
#[clap(visible_alias = "cl")]
Clone {
repo_url: String,
#[clap(long)]
name: Option<String>,
#[clap(long)]
path: Option<PathBuf>,
#[clap(flatten)]
zellij_options: ZellijOptions,
},
#[clap(visible_alias = "r")]
Root,
#[clap(visible_alias = "p")]
Preview {
target: String,
},
}
#[derive(Debug, Serialize)]
struct ListEntry {
src: String,
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
score: Option<f64>,
}
fn shorten_home(path: &Path) -> String {
if let Some(home) = dirs::home_dir()
&& let Ok(suffix) = path.strip_prefix(&home)
{
return format!("~/{}", suffix.display());
}
path.display().to_string()
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let cli = Cli::parse();
let zellij = ZellijClient::new();
let zoxide = ZoxideClient::new();
let fs = RealFs::new();
let git = RealGit;
let connect_service = ConnectService::new(zellij, zoxide, fs, git);
match &cli.command {
Commands::List {
zesh,
zoxide: zoxide_only,
json,
hide_attached,
hide_duplicates,
} => {
let show_all = !zesh && !zoxide_only;
let show_zellij = show_all || *zesh;
let show_zoxide = show_all || *zoxide_only;
let mut entries: Vec<ListEntry> = Vec::new();
if show_zellij {
let sessions = zellij.list_sessions()?;
for session in &sessions {
if *hide_attached && session.is_current {
continue;
}
entries.push(ListEntry {
src: "zellij".to_string(),
name: session.name.clone(),
path: None,
score: None,
});
}
}
if show_zoxide {
let zoxide_entries = zoxide.list()?;
for entry in &zoxide_entries {
entries.push(ListEntry {
src: "zoxide".to_string(),
name: shorten_home(&entry.path),
path: Some(entry.path.display().to_string()),
score: Some(entry.score),
});
}
}
if *hide_duplicates {
let mut seen = HashSet::new();
entries.retain(|e| seen.insert(e.name.clone()));
}
if *json {
let json_str = serde_json::to_string(&entries)?;
println!("{}", json_str);
} else {
for entry in &entries {
println!("{}", entry.name);
}
}
}
Commands::Connect {
name,
zellij_options,
} => {
if let Err(e) = connect_service.connect(name, zellij_options) {
eprintln!("Error connecting to '{}': {}", name, e);
return Err(e.into());
}
}
Commands::Clone {
repo_url,
name,
path,
zellij_options,
} => {
let clone_service = CloneService::new(zellij, zoxide, fs, git);
if let Err(e) =
clone_service.clone_repo(repo_url, name.as_deref(), path.as_ref(), zellij_options)
{
eprintln!("Clone failed: {}", e);
return Err(e.into());
}
}
Commands::Root => {
let sessions = zellij.list_sessions()?;
let current = sessions.iter().find(|s| s.is_current);
if let Some(_session) = current {
println!("{}", env::current_dir()?.display());
} else {
println!("No active zellij session");
}
}
Commands::Preview { target } => {
let sessions = zellij.list_sessions()?;
let session_match = sessions.iter().find(|s| s.name == *target);
if let Some(session) = session_match {
println!("Session: {}", session.name);
return Ok(());
}
let path = PathBuf::from(target);
if path.is_dir() {
println!("Directory: {}", path.display());
preview_directory(&path)?;
return Ok(());
}
let entries = zoxide.query(&[target])?;
if entries.is_empty() {
println!("No matching sessions or directories found for '{}'", target);
return Ok(());
}
let best_match = &entries[0];
println!("Directory (via zoxide): {}", best_match.path.display());
preview_directory(&best_match.path)?;
}
}
Ok(())
}
fn preview_directory(path: &Path) -> Result<(), Box<dyn std::error::Error>> {
let entries = fs::read_dir(path)?;
for entry in entries {
let entry = entry?;
let metadata = entry.metadata()?;
let file_type = if metadata.is_dir() {
"dir"
} else if metadata.is_file() {
"file"
} else {
"other"
};
println!("{:<6} {}", file_type, entry.file_name().to_string_lossy());
}
Ok(())
}