pub mod commands;
pub mod features;
use crate::commands::curl;
use crate::commands::diag::check_nginx_mismatches;
use crate::commands::generate_systemd::{run_generate_systemd, GenerateSystemdArgs};
use crate::commands::pm2_save;
use crate::commands::redeploy_v2::run_redeploy_v2;
use crate::commands::ssh_logs::run_remote_logs;
use crate::commands::{install_package, run_config, run_ports, run_redeploy, run_setup};
use crate::commands::{
is_xbp_project, list_services, open_global_config, pm2_env, pm2_flush, pm2_monitor,
pm2_resurrect, pm2_snapshot, print_help, run_login, run_redeploy_service, run_service_command,
run_version_command, show_service_help,
};
use crate::commands::{kafka_logs::LogConfig, start_log_shipping, tail_kafka_topic};
use crate::commands::{pm2_list, pm2_logs, pm2_start_wrapper};
use crate::commands::{run_diag, run_nginx, run_secrets};
use crate::commands::{run_single_check, start_monitor_daemon};
use crate::config::sync_versioning_files_registry;
use crate::logging::{get_log_directory, init_logger, log_error, log_info, log_success, log_warn};
use crate::profile::{find_all_xbp_projects, rank_projects_by_proximity, Profile, ProjectInfo};
use crate::project_detector::ProjectType;
use crate::project_detector::{
detect_project_types, display_project_types, display_suggested_commands,
};
use crate::utils::command_exists;
use clap::Parser;
use colored::Colorize;
use commands::{Cli, Commands};
use crossterm::{
cursor::MoveToColumn,
event::{self, Event, KeyCode},
style::{Color, ResetColor, SetForegroundColor},
terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType},
ExecutableCommand,
};
use dialoguer::Input;
use std::env;
use std::io::{stdout, Write};
use std::process::Command;
#[cfg(feature = "monitoring")]
use xbp_monitoring::{list_items, run_all_once, serve, RunKind};
pub async fn run() -> Result<(), String> {
let cli: Cli = Cli::parse();
let debug: bool = cli.debug;
if let Err(e) = init_logger(debug).await {
let _ = log_error(
"system",
"Failed to initialize logger",
Some(&e.to_string()),
)
.await;
}
if let Err(e) = sync_versioning_files_registry() {
let _ = log_warn("config", "Failed to sync versioning registry", Some(&e)).await;
}
if cli.logs {
return handle_logs_flag().await;
}
if cli.list && cli.command.is_none() {
if let Err(e) = pm2_list(debug).await {
let _ = log_error("pm2", "pm2 list failed", Some(&e)).await;
}
if let Err(e) = show_systemd_status(debug).await {
if debug {
let _ = log_warn("systemd", "Could not check systemd status", Some(&e)).await;
}
}
return Ok(());
}
match cli.command {
Some(Commands::Ports(cmd)) => handle_ports(cmd, cli.port, debug).await,
Some(Commands::Setup) => handle_setup(debug).await,
Some(Commands::Redeploy { service_name }) => handle_redeploy(service_name, debug).await,
Some(Commands::RedeployV2(cmd)) => handle_redeploy_v2(cmd, debug).await,
Some(Commands::Config(cmd)) => handle_config(cmd, debug).await,
Some(Commands::Install { package }) => handle_install(package, debug).await,
Some(Commands::Logs(cmd)) => handle_logs(cmd, debug).await,
Some(Commands::List) => handle_list(debug).await,
Some(Commands::Curl(cmd)) => handle_curl(cmd, debug).await,
Some(Commands::Services) => handle_services(debug).await,
Some(Commands::Service {
command,
service_name,
}) => handle_service(command, service_name, debug).await,
Some(Commands::Nginx(cmd)) => handle_nginx(cmd.command, debug).await,
Some(Commands::Diag(cmd)) => handle_diag(cmd, debug).await,
Some(Commands::Monitor(cmd)) => handle_monitor(cmd, debug).await,
Some(Commands::Snapshot) => handle_snapshot(debug).await,
Some(Commands::Resurrect) => handle_resurrect(debug).await,
Some(Commands::Stop { target }) => handle_stop(target, debug).await,
Some(Commands::Flush { target }) => handle_flush(target, debug).await,
Some(Commands::Login) => handle_login().await,
Some(Commands::Version(cmd)) => handle_version(cmd, debug).await,
Some(Commands::Env { target }) => handle_env(target, debug).await,
Some(Commands::Tail(cmd)) => handle_tail(cmd, debug).await,
Some(Commands::Start { args }) => handle_start(args, debug).await,
Some(Commands::Generate(cmd)) => handle_generate(cmd, debug).await,
Some(Commands::Secrets(cmd)) => handle_secrets(cmd, debug).await,
#[cfg(feature = "monitoring")]
Some(Commands::Monitoring(cmd)) => handle_monitoring(cmd).await,
None => handle_no_command(cli.port, debug).await,
}
}
async fn handle_start(args: Vec<String>, debug: bool) -> Result<(), String> {
if let Err(e) = pm2_start_wrapper(args, debug).await {
let _ = log_error("start", "Failed to start process", Some(&e)).await;
return Err(e);
}
if let Err(e) = pm2_save(debug).await {
let _ = log_error("start", "pm2 save failed", Some(&e)).await;
return Err(e);
}
if let Err(e) = pm2_list(debug).await {
let _ = log_error("start", "pm2 list failed", Some(&e)).await;
return Err(e);
}
Ok(())
}
async fn handle_logs_flag() -> Result<(), String> {
let log_dir = get_log_directory().await?;
println!(
"{} {}",
"Logs directory:".bright_blue(),
log_dir.display().to_string().cyan()
);
if cfg!(target_os = "windows") {
println!("\n{}", "Opening in Explorer...".dimmed());
let _ = Command::new("explorer").arg(log_dir).spawn();
} else {
println!("\n{}", "To view logs:".bright_blue());
println!(" cd {}", log_dir.display().to_string().cyan());
println!(" tail -f xbp-*.log");
}
Ok(())
}
async fn handle_ports(
cmd: commands::PortsCmd,
global_port: Option<u16>,
debug: bool,
) -> Result<(), String> {
let mut args: Vec<String> = Vec::new();
let port: Option<u16> = global_port.or(cmd.port);
if let Some(p) = port {
args.push("-p".to_string());
args.push(p.to_string());
}
if cmd.kill {
args.push("--kill".to_string());
args.push("-k".to_string());
}
if cmd.nginx {
args.push("-n".to_string());
args.push("--nginx".to_string());
}
if cmd.full {
args.push("--full".to_string());
}
if let Err(e) = run_ports(&args, debug).await {
let _ = log_error("ports", "Error running ports", Some(&e)).await;
}
Ok(())
}
async fn handle_setup(debug: bool) -> Result<(), String> {
if let Err(e) = run_setup(debug).await {
let _ = log_error("setup", "Setup failed", Some(&e)).await;
}
Ok(())
}
async fn handle_redeploy(service_name: Option<String>, debug: bool) -> Result<(), String> {
if let Some(name) = service_name {
if let Err(e) = run_redeploy_service(&name, debug).await {
let _ = log_error("redeploy", "Service redeploy failed", Some(&e)).await;
}
} else {
if let Err(e) = run_redeploy().await {
let _ = log_error("redeploy", "Redeploy failed", Some(&e)).await;
}
}
Ok(())
}
async fn handle_redeploy_v2(cmd: commands::RedeployV2Cmd, debug: bool) -> Result<(), String> {
let _ = log_info("redeploy_v2", "Starting remote redeploy process", None).await;
match run_redeploy_v2(cmd.password, cmd.username, cmd.host, cmd.project_dir, debug).await {
Ok(()) => Ok(()),
Err(e) => {
let _ = log_error("redeploy_v2", "Remote redeploy failed", Some(&e)).await;
Err(e)
}
}
}
async fn handle_config(cmd: commands::ConfigCmd, debug: bool) -> Result<(), String> {
if cmd.project {
let _ = run_config(debug).await;
} else if let Err(e) = open_global_config(cmd.no_open).await {
let _ = log_error("config", "Failed to open global config", Some(&e)).await;
}
Ok(())
}
async fn handle_install(package: String, debug: bool) -> Result<(), String> {
if package.is_empty() || package == "--help" || package == "help" {
return install_package("", debug).await;
}
let install_msg: String = format!("Installing package: {}", package);
let _ = log_info("install", &install_msg, None).await;
match install_package(&package, debug).await {
Ok(()) => {
let success_msg = format!("Successfully installed: {}", package);
let _ = log_success("install", &success_msg, None).await;
Ok(())
}
Err(e) => Err(e),
}
}
async fn handle_logs(cmd: commands::LogsCmd, debug: bool) -> Result<(), String> {
let commands::LogsCmd {
project,
ssh_host,
ssh_username,
ssh_password,
} = cmd;
let remote_requested = ssh_host.is_some() || ssh_username.is_some() || ssh_password.is_some();
if remote_requested {
if let Err(e) = run_remote_logs(project, ssh_host, ssh_username, ssh_password, debug).await
{
let _ = log_error("logs", "Remote logs failed", Some(&e)).await;
}
return Ok(());
}
if let Err(e) = pm2_logs(project, debug).await {
let _ = log_error("pm2", "pm2 logs failed", Some(&e)).await;
}
Ok(())
}
async fn show_systemd_status(debug: bool) -> Result<(), String> {
use crate::commands::service::load_xbp_config;
use crate::strategies::get_all_services;
if !cfg!(target_os = "linux") || !command_exists("systemctl") {
return Ok(());
}
let config = match load_xbp_config().await {
Ok(cfg) => cfg,
Err(_) => return Ok(()), };
let mut systemd_services = Vec::new();
if let Some(ref name) = config.systemd_service_name {
systemd_services.push(name.clone());
}
let services = get_all_services(&config);
for service in services {
if let Some(ref name) = service.systemd_service_name {
if !systemd_services.contains(name) {
systemd_services.push(name.clone());
}
}
}
if systemd_services.is_empty() {
return Ok(()); }
println!("\nSystemd Services:");
println!("{}", "═".repeat(60));
for service_name in systemd_services {
check_systemd_service_status(&service_name, debug).await;
}
Ok(())
}
async fn check_systemd_service_status(service_name: &str, debug: bool) {
use tokio::process::Command;
let output = Command::new("systemctl")
.arg("status")
.arg(service_name)
.arg("--no-pager")
.output()
.await;
match output {
Ok(output) => {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
if stderr.contains("could not be found") || stderr.contains("not loaded") {
println!(" {} - {}", service_name, "Not Found".dimmed());
if debug {
println!(" Service unit not found in systemd");
}
return;
}
if stderr.contains("Permission denied") || stderr.contains("Failed to get properties") {
println!(" {} - {}", service_name, "Permission Denied".yellow());
if debug {
println!(" Run with sudo to see full status");
}
return;
}
let status = if stdout.contains("Active: active (running)") {
"Running".green()
} else if stdout.contains("Active: inactive (dead)") {
"Stopped".dimmed()
} else if stdout.contains("Active: failed") {
"Failed".red()
} else if stdout.contains("Active: activating") {
"Starting".yellow()
} else if stdout.contains("Active: deactivating") {
"Stopping".yellow()
} else {
"Unknown".yellow()
};
println!(" {} - {}", service_name, status);
if debug {
let lines: Vec<&str> = stdout.lines().take(3).collect();
for line in lines {
println!(" {}", line.trim());
}
}
}
Err(e) => {
println!(" {} - {}", service_name, "Error".red());
if debug {
println!(" Failed to check status: {}", e);
}
}
}
}
async fn handle_list(debug: bool) -> Result<(), String> {
if let Err(e) = pm2_list(debug).await {
let _ = log_error("pm2", "pm2 list failed", Some(&e)).await;
}
if let Err(e) = show_systemd_status(debug).await {
if debug {
let _ = log_warn("systemd", "Could not check systemd status", Some(&e)).await;
}
}
Ok(())
}
async fn handle_curl(cmd: commands::CurlCmd, debug: bool) -> Result<(), String> {
let url = cmd
.url
.unwrap_or_else(|| "https://example.com/api".to_string());
if let Err(e) = curl::run_curl(&url, cmd.no_timeout, debug).await {
let _ = log_error("curl", "Curl command failed", Some(&e)).await;
}
Ok(())
}
async fn handle_services(debug: bool) -> Result<(), String> {
if let Err(e) = list_services(debug).await {
let _ = log_error("services", "Failed to list services", Some(&e)).await;
}
Ok(())
}
async fn handle_service(
command: Option<String>,
service_name: Option<String>,
debug: bool,
) -> Result<(), String> {
if let Some(cmd) = command {
if cmd == "--help" || cmd == "help" {
if let Some(name) = service_name {
if let Err(e) = show_service_help(&name).await {
let _ = log_error("service", "Failed to show service help", Some(&e)).await;
}
} else {
println!("Usage: xbp service <command> <service-name>");
println!("Commands: build, install, start, dev");
println!("Example: xbp service build zeus");
println!("For help on a specific service: xbp service --help <service-name>");
}
} else {
if let Some(name) = service_name {
if let Err(e) = run_service_command(&cmd, &name, debug).await {
let _ = log_error(
"service",
&format!("Service command '{}' failed", cmd),
Some(&e),
)
.await;
}
} else {
let _ = log_error("service", "Service name required", None).await;
}
}
} else {
println!("Usage: xbp service <command> <service-name>");
println!("Commands: build, install, start, dev");
println!("Example: xbp service build zeus");
}
Ok(())
}
async fn handle_nginx(cmd: commands::NginxSubCommand, debug: bool) -> Result<(), String> {
if let Err(e) = run_nginx(cmd, debug).await {
let _ = log_error("nginx", "Nginx command failed", Some(&e.to_string())).await;
}
Ok(())
}
async fn handle_diag(cmd: commands::DiagCmd, debug: bool) -> Result<(), String> {
if let Err(e) = run_diag(cmd, debug).await {
let _ = log_error("diag", "Diag command failed", Some(&e.to_string())).await;
}
Ok(())
}
async fn handle_generate(cmd: commands::GenerateCmd, debug: bool) -> Result<(), String> {
match cmd.command {
commands::GenerateSubCommand::Systemd(subcmd) => {
let args = GenerateSystemdArgs {
output_dir: subcmd.output_dir,
service: subcmd.service,
};
if let Err(e) = run_generate_systemd(args, debug).await {
let _ = log_error(
"generate-systemd",
"Failed to generate systemd units",
Some(&e),
)
.await;
}
}
}
Ok(())
}
async fn handle_secrets(cmd: commands::SecretsCmd, debug: bool) -> Result<(), String> {
if let Err(e) = run_secrets(cmd, debug).await {
let _ = log_error("secrets", "Secrets command failed", Some(&e)).await;
}
Ok(())
}
async fn handle_monitor(cmd: commands::MonitorCmd, debug: bool) -> Result<(), String> {
match cmd.command {
Some(commands::MonitorSubCommand::Check) => {
if !is_xbp_project().await {
return handle_project_selection().await;
}
if let Err(e) = run_single_check().await {
let _ = log_error("monitor", "Monitor check failed", Some(&e.to_string())).await;
}
}
Some(commands::MonitorSubCommand::Start) => {
if !is_xbp_project().await {
return handle_project_selection().await;
}
if let Err(e) = start_monitor_daemon().await {
let _ = log_error("monitor", "Monitor daemon failed", Some(&e.to_string())).await;
}
}
None => {
if let Err(e) = pm2_monitor(debug).await {
let _ = log_error("monitor", "PM2 monitor failed", Some(&e)).await;
}
}
}
Ok(())
}
async fn handle_snapshot(debug: bool) -> Result<(), String> {
match pm2_snapshot(debug).await {
Ok(path) => {
let _ = log_info(
"snapshot",
"Saved PM2 snapshot",
Some(&path.display().to_string()),
)
.await;
println!("Saved PM2 snapshot to {}", path.display());
Ok(())
}
Err(e) => {
let _ = log_error("snapshot", "PM2 snapshot failed", Some(&e)).await;
Err(e)
}
}
}
async fn handle_resurrect(debug: bool) -> Result<(), String> {
if let Err(e) = pm2_resurrect(debug).await {
let _ = log_error("pm2", "pm2 resurrect failed", Some(&e)).await;
}
Ok(())
}
async fn handle_stop(target: Option<String>, debug: bool) -> Result<(), String> {
let target = target.unwrap_or_else(|| "all".to_string());
if let Err(e) = crate::commands::pm2_stop(&target, debug).await {
let _ = log_error("pm2", "pm2 stop failed", Some(&e)).await;
}
Ok(())
}
async fn handle_flush(target: Option<String>, debug: bool) -> Result<(), String> {
if let Err(e) = pm2_flush(target.as_deref(), debug).await {
let _ = log_error("pm2", "pm2 flush failed", Some(&e)).await;
}
Ok(())
}
async fn handle_env(target: String, debug: bool) -> Result<(), String> {
if let Err(e) = pm2_env(&target, debug).await {
let _ = log_error("pm2", "pm2 env failed", Some(&e)).await;
}
Ok(())
}
async fn handle_login() -> Result<(), String> {
if let Err(e) = run_login().await {
let _ = log_error("login", "Login failed", Some(&e)).await;
}
Ok(())
}
async fn handle_version(cmd: commands::VersionCmd, debug: bool) -> Result<(), String> {
if let Err(e) = run_version_command(cmd.target, cmd.git, debug).await {
let _ = log_error("version", "Version command failed", Some(&e)).await;
return Err(e);
}
Ok(())
}
#[cfg(feature = "monitoring")]
async fn handle_monitoring(cmd: commands::MonitoringCmd) -> Result<(), String> {
match cmd.command {
commands::MonitoringSubCommand::Serve { file } => {
if let Err(err) = serve(&file).await {
return Err(format!("Failed to start monitoring service: {}", err));
}
}
commands::MonitoringSubCommand::RunOnce {
file,
probes_only,
stories_only,
} => {
let results = run_all_once(&file, probes_only, stories_only)
.await
.map_err(|e| e.to_string())?;
if results.is_empty() {
println!("No probes or stories configured");
} else {
for result in results {
let label = match result.kind {
RunKind::Probe => "probe",
RunKind::Story => "story",
};
if result.success {
println!("{} {} ✓", label, result.name);
} else {
let code = result
.status_code
.map(|c| c.to_string())
.unwrap_or_else(|| "-".into());
let err = result.error.unwrap_or_else(|| "Unknown error".into());
println!("{} {} ✗ (code {}, {})", label, result.name, code, err);
}
}
}
}
commands::MonitoringSubCommand::List { file } => {
let (probes, stories) = list_items(&file).await.map_err(|e| e.to_string())?;
println!(
"Probes: {}",
if probes.is_empty() {
"none".into()
} else {
probes.join(", ")
}
);
println!(
"Stories: {}",
if stories.is_empty() {
"none".into()
} else {
stories.join(", ")
}
);
}
}
Ok(())
}
async fn handle_tail(cmd: commands::TailCmd, _debug: bool) -> Result<(), String> {
if cmd.kafka {
match LogConfig::from_xbp_config().await {
Ok(Some(config)) => {
if let Err(e) = tail_kafka_topic(&config).await {
let _ =
log_error("tail", "Failed to tail Kafka topic", Some(&e.to_string())).await;
}
}
Ok(None) => {
let _ = log_error("tail", "No log configuration found in xbp.json", None).await;
}
Err(e) => {
let _ =
log_error("tail", "Failed to load configuration", Some(&e.to_string())).await;
}
}
} else if cmd.ship {
if let Err(e) = start_log_shipping().await {
let _ = log_error("tail", "Failed to ship logs", Some(&e.to_string())).await;
}
} else {
if let Err(e) = start_log_shipping().await {
let _ = log_error("tail", "Failed to tail logs", Some(&e.to_string())).await;
}
}
Ok(())
}
async fn handle_no_command(port: Option<u16>, debug: bool) -> Result<(), String> {
if let Some(port) = port {
let args = vec!["-p".to_string(), port.to_string()];
if let Err(e) = run_ports(&args, debug).await {
let _ = log_error("ports", "Error running ports", Some(&e)).await;
}
return Ok(());
}
if is_xbp_project().await {
let current_dir: std::path::PathBuf = env::current_dir().unwrap_or_default();
if let Ok(mut profile) = Profile::load() {
if let Some(project_name) = current_dir
.file_name()
.and_then(|n| n.to_str())
.map(|s| s.to_string())
{
profile.update_last_project(current_dir.clone(), project_name);
let _ = profile.save();
}
}
println!("{}", "XBP Project Detected".bright_green());
let project_types: Vec<ProjectType> = detect_project_types(¤t_dir);
display_project_types(&project_types);
match check_nginx_mismatches().await {
Ok(mismatches) => {
if !mismatches.is_empty() {
let _ = log_warn(
"diag",
&format!(
"Found {} Nginx port mismatch(es). Run 'xbp diag' to fix.",
mismatches.len()
),
None,
)
.await;
}
}
Err(_) => {}
}
if let Err(e) = list_services(debug).await {
let _ = log_error("services", "Failed to list services", Some(&e)).await;
}
display_suggested_commands(&project_types);
println!("\n{}", "Available Commands:".bright_blue());
println!("{} - List all services", "xbp services".bright_magenta());
println!(
"{} - Run command for a service",
"xbp service <cmd> <name>".bright_magenta()
);
println!(
"{} - Redeploy a service",
"xbp redeploy <name>".bright_magenta()
);
println!("{} - Show configuration", "xbp config".bright_magenta());
println!(
"{} - Save the PM2 process snapshot",
"xbp snapshot".bright_magenta()
);
println!(
"{} - Inspect and sync versions",
"xbp version".bright_magenta()
);
println!(
"{} - Diagnose and fix configuration issues",
"xbp diag".bright_magenta()
);
println!("For more help: {}", "xbp --help".yellow());
} else {
handle_project_selection().await?;
}
Ok(())
}
async fn handle_project_selection() -> Result<(), String> {
let profile: Profile = Profile::load().unwrap_or_default();
if let Some(last_path) = &profile.last_project_path {
if last_path.exists() {
let project_name: &str = last_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown");
println!("\n📌 Last XBP project:");
println!(" Name: {}", project_name.purple());
println!(" Path: {}", last_path.display());
println!(
"\n{} {}",
"📌".bright_blue(),
"Last XBP project:".bright_blue().bold()
);
println!(
" {} {}",
"Name:".dimmed(),
project_name.bright_cyan().bold()
);
println!(
" {} {}",
"Path:".dimmed(),
last_path.display().to_string().bright_black()
);
let response: String = Input::new()
.with_prompt(format!(
"{} Continue with this project? (Y/n/a)",
"→".bright_blue()
))
.default("Y".to_string())
.interact_text()
.unwrap_or_else(|_| "n".to_string());
match response.to_lowercase().as_str() {
"y" | "" => {
println!(
"\n{} {} {}",
"✓".bright_green(),
"Switching to:".bright_green().bold(),
project_name.bright_cyan()
);
println!("\n{} {}", "💡".yellow(), "To navigate:".yellow());
println!(
" {}",
format!("cd {}", last_path.display()).bright_cyan().bold()
);
return Ok(());
}
"a" => {
return show_all_projects().await;
}
_ => {
print_help().await;
return Ok(());
}
}
}
}
println!(
"\n{} {}",
"⚠️".yellow(),
"Currently not in an XBP project".yellow().bold()
);
println!(
" {} {}",
"→".dimmed(),
"No xbp.json found in current directory or .xbp/xbp.json".dimmed()
);
println!("\n{}", "Options:".bright_blue().bold());
println!(
" {} Run {} to initialize a new project",
"•".bright_blue(),
"xbp setup".bright_cyan()
);
println!(
" {} Run {} to see all available XBP projects",
"•".bright_blue(),
"xbp".bright_cyan()
);
let response: String = Input::new()
.with_prompt(format!(
"{} Show all XBP projects? (y/N)",
"→".bright_blue()
))
.default("N".to_string())
.interact_text()
.unwrap_or_else(|_| "N".to_string());
if response.to_lowercase() == "y" {
show_all_projects().await?;
} else {
print_help().await;
}
Ok(())
}
async fn show_all_projects() -> Result<(), String> {
use colored::Colorize;
println!(
"\n{} {}",
"🔍".bright_blue(),
"Searching for XBP projects...".bright_blue().bold()
);
let projects: Vec<ProjectInfo> = find_all_xbp_projects();
if projects.is_empty() {
println!("\nNo XBP projects found.");
println!("Run 'xbp setup' to initialize a new project.");
println!(
"\n{} {}",
"⚠️".yellow(),
"No XBP projects found.".yellow().bold()
);
println!(
" {} {}",
"→".dimmed(),
format!(
"Run {} to initialize a new project.",
"xbp setup".bright_cyan()
)
);
return Ok(());
}
let current_dir: std::path::PathBuf = env::current_dir().unwrap_or_default();
let ranked_projects: Vec<ProjectInfo> =
rank_projects_by_proximity(projects, current_dir.clone());
println!("\nXBP Projects ({} found):", ranked_projects.len());
println!(
"\n{} {} {} {}",
"📦".bright_green(),
"XBP Projects".bright_green().bold(),
format!("({})", ranked_projects.len()).bright_black(),
if ranked_projects.len() > 1 {
"found"
} else {
"found"
}
);
println!("{}", "─".repeat(60).bright_black());
let items: Vec<String> = ranked_projects
.iter()
.enumerate()
.map(|(i, p)| {
let is_current = p.path == current_dir;
let number = format!("{:2}", i + 1).bright_black();
let name = if is_current {
format!("{} {}", p.name, "★".yellow())
} else {
p.name.clone()
};
let path_display: String = if p.path.starts_with(dirs::home_dir().unwrap_or_default()) {
p.path
.strip_prefix(dirs::home_dir().unwrap_or_default())
.map(|p| format!("~/{}", p.display()))
.unwrap_or_else(|_| p.path.display().to_string())
} else {
p.path.display().to_string()
};
let line = format!(
"{} {} {}",
number,
name.bright_cyan().bold(),
format!("({})", path_display).bright_black()
);
println!("{}", line);
line
})
.collect();
if items.is_empty() {
return Ok(());
}
println!("\nUse ← and → to select a project, Enter to confirm, Esc to cancel.\n");
interactive_project_selector(&ranked_projects)?;
Ok(())
}
fn interactive_project_selector(projects: &[ProjectInfo]) -> Result<(), String> {
if projects.is_empty() {
return Ok(());
}
let mut index: usize = 0;
let mut out: std::io::Stdout = stdout();
enable_raw_mode().map_err(|e| e.to_string())?;
loop {
out.execute(Clear(ClearType::CurrentLine))
.map_err(|e| e.to_string())?;
out.execute(MoveToColumn(0)).map_err(|e| e.to_string())?;
for (i, project) in projects.iter().enumerate() {
if i == index {
out.execute(SetForegroundColor(Color::Green))
.map_err(|e| e.to_string())?;
write!(out, " [{}] ", project.name).map_err(|e| e.to_string())?;
out.execute(ResetColor).map_err(|e| e.to_string())?;
} else {
write!(out, " {} ", project.name).map_err(|e| e.to_string())?;
}
}
out.flush().map_err(|e| e.to_string())?;
match event::read().map_err(|e| e.to_string())? {
Event::Key(key) => match key.code {
KeyCode::Left => {
if index == 0 {
index = projects.len().saturating_sub(1);
} else {
index -= 1;
}
}
KeyCode::Right => {
index = (index + 1) % projects.len();
}
KeyCode::Enter => {
disable_raw_mode().ok();
let selected: &ProjectInfo = &projects[index];
println!("\nSelected: {}", selected.name);
println!("Path: {}", selected.path.display());
println!("To navigate: cd {}", selected.path.display());
println!(
"\n{} {} {}",
"✓".bright_green(),
"Selected:".bright_green().bold(),
selected.name.bright_cyan()
);
println!(
" {} {}",
"Path:".dimmed(),
selected.path.display().to_string().bright_white()
);
println!(
"\n{} {}",
"💡".yellow(),
"To navigate to this project:".yellow()
);
println!(
" {}",
format!("cd {}", selected.path.display())
.bright_cyan()
.bold()
);
return Ok(());
}
KeyCode::Esc => {
disable_raw_mode().ok();
println!();
return Ok(());
}
_ => {}
},
_ => {}
}
}
}