use crate::{
App, Shell, UserConfig, ZigVersion, suggest,
tools::{self, error},
};
use clap::{Parser, Subcommand};
use color_eyre::eyre::eyre;
use std::str::FromStr;
use yansi::Paint;
mod clean;
mod init;
mod install;
mod list;
mod setup;
pub mod sync; mod update;
mod r#use;
mod zig;
mod zls;
pub use zig::zig_main;
pub use zls::zls_main;
#[derive(Debug, Clone)]
pub enum CleanTarget {
All,
Downloads,
Versions(Vec<ZigVersion>),
}
fn parse_clean_target(s: &str) -> Result<CleanTarget, String> {
match s.to_lowercase().as_str() {
"all" => Ok(CleanTarget::All),
"downloads" => Ok(CleanTarget::Downloads),
_ => {
let versions: Result<Vec<ZigVersion>, _> = s
.split(',')
.map(|v| ZigVersion::from_str(v.trim()))
.collect();
match versions {
Ok(vers) if !vers.is_empty() => Ok(CleanTarget::Versions(vers)),
Ok(_) => Err("No valid versions provided".to_string()),
Err(e) => Err(format!("Invalid version format: {}", e)),
}
}
}
}
pub async fn zv_main() -> super::Result<()> {
let zv_cli = <ZvCli as clap::Parser>::parse();
let (zv_base_path, using_env) = tools::fetch_zv_dir()?;
if using_env {
tracing::debug!("Using ZV_DIR from environment: {}", zv_base_path.display());
}
let app = App::init(UserConfig {
zv_base_path,
shell: Some(Shell::detect()),
})
.await?;
match zv_cli.command {
Some(cmd) => cmd.execute(app, using_env).await?,
None => {
print_welcome_message(app);
}
}
Ok(())
}
#[derive(Parser, Debug)]
#[command(name = "zv")]
#[command(
author,
version,
about = "zv - A Zig Version Manager",
long_about = "A fast, easy to use, Zig programming language installer and version manager. \
To find out more, run `-h` or `--help` with the subcommand you're interested in. Example: `zv install -h` for short help \
or `zv install --help` for long help."
)]
pub struct ZvCli {
#[command(subcommand)]
pub(crate) command: Option<Commands>,
}
#[derive(Subcommand, Debug)]
pub enum Commands {
Init {
project_name: Option<String>,
#[arg(
long = "zig",
short = 'z',
help = "Use `zig init` instead to create a new Zig project",
conflicts_with = "package"
)]
zig: bool,
#[arg(
long = "package",
alias = "zon",
short = 'p',
help = "Enable package support via build.zig.zon file",
conflicts_with = "zig"
)]
package: bool,
},
#[clap(alias = "i")]
Install {
#[arg(
long = "force-ziglang",
short = 'f',
long_help = "Force using ziglang.org as a download source. Default is to use community mirrors."
)]
force_ziglang: bool,
#[arg(
value_delimiter = ',',
value_parser = clap::value_parser!(ZigVersion),
help = "The version(s) of Zig to install. Use 'master', 'stable@<version>', 'stable', 'latest', or simply <version> (e.g., '0.15.1'). Multiple versions can be comma-separated.",
long_help = "The version(s) of Zig to install. Options:\n\
• master - Install master branch build\n\
• <semver> - Install specific version (e.g., 0.13.0, 1.2.3)\n\
• stable@<version> - Install specific stable version. Identical to just <version> (e.g., stable@0.13.0)\n\
• stable - Install latest stable release\n\
• latest - Install latest stable release (queries network instead of relying on cached index)\n\
Multiple versions can be specified as comma-separated values."
)]
versions: Vec<ZigVersion>,
},
Use {
#[arg(
long = "force-ziglang",
short = 'f',
long_help = "Force using ziglang.org as a download source. Default is to use community mirrors."
)]
force_ziglang: bool,
#[arg(
value_parser = clap::value_parser!(ZigVersion),
help = "The version of Zig to use. Use 'master', 'stable@<version>', 'stable', 'latest', or simply <version> (e.g., '0.15.1')",
long_help = "The version of Zig to use. Options:\n\
• master - Use master branch build\n\
• <semver> - Use specific version (e.g., 0.13.0, 1.2.3)\n\
• stable@<version> - Use specific stable version. Identical to just <version> (e.g., stable@0.13.0)\n\
• stable - Use latest stable release\n\
• latest - Use latest stable release (queries network instead of relying on cached index)"
)]
version: Option<ZigVersion>,
},
#[clap(name = "list", alias = "ls")]
List,
#[clap(name = "clean", alias = "rm")]
Clean {
#[arg(
long = "except",
value_delimiter = ',',
value_parser = clap::value_parser!(ZigVersion),
help = "Clean all except specified versions (comma-separated)",
long_help = "Clean all installed versions except the ones specified.\n\
Accepts comma-separated list of versions.\n\
Examples: --except 0.13.0,0.14.0 or --except master"
)]
except: Vec<ZigVersion>,
#[arg(
long = "outdated",
help = "Clean outdated master versions (keeps latest)",
long_help = "Clean outdated master versions, keeping only the latest.\n\
If used with a target 'master', cleans master versions.\n\
If used alone, defaults to cleaning master versions."
)]
outdated: bool,
#[arg(
value_parser = parse_clean_target,
help = "What to clean: 'all', 'downloads', version(s), or omit for all",
long_help = "Specify what to clean:\n\
• all - Clean everything\n\
• downloads - Clean downloads directory only\n\
• <version> - Clean specific version (e.g., 0.13.0, master)\n\
• <v1,v2,...> - Clean multiple versions (comma-separated)\n\
• master - Clean all master versions (use with --outdated to keep latest)"
)]
target: Option<CleanTarget>,
},
Setup {
#[arg(
long,
alias = "dry",
short = 'd',
help = "Preview changes without applying them"
)]
dry_run: bool,
#[arg(
long = "no-interactive",
help = "Disable interactive prompts and use default choices for automation",
long_help = "Disable interactive prompts and use default choices for automation.\n\
Interactive mode is automatically disabled in CI environments,\n\
when TERM=dumb, or when TTY is not available."
)]
no_interactive: bool,
},
#[clap(alias = "upgrade")]
Update {
#[arg(
long,
short = 'f',
help = "Force update even if the current version is the latest"
)]
force: bool,
#[arg(
long,
help = "Include pre-release versions when checking for updates"
)]
rc: bool,
},
Sync,
}
impl Commands {
pub(crate) async fn execute(self, mut app: App, using_env: bool) -> super::Result<()> {
match self {
Commands::Init {
project_name,
zig,
package: zon,
} => {
use crate::{Template, TemplateType};
if zig {
init::init_project(
Template::new(
project_name,
TemplateType::Zig(app.zv_zig().ok_or_else(|| {
tools::error("Cannot use `zig init` for template instantiation.");
suggest!(
"You can install a compatible Zig version with {}",
cmd = "zv use <version>"
);
suggest!(
"Also make sure you've run {} to set up your shell environment",
cmd = "zv setup"
);
eyre!("No Zig executable found")
})?),
),
app,
)
.await
} else {
init::init_project(Template::new(project_name, TemplateType::App { zon }), app)
.await
}
}
Commands::Use {
version,
force_ziglang,
} => match version {
Some(version) => r#use::use_version(version, &mut app, force_ziglang).await,
None => {
error("Version must be specified. e.g., `zv use latest` or `zv use 0.15.1`");
std::process::exit(2);
}
},
Commands::Install {
versions,
force_ziglang,
} => install::install_versions(versions, &mut app, force_ziglang).await,
Commands::List => list::list_versions(&mut app).await,
Commands::Clean {
except,
outdated,
target,
} => clean::clean(&mut app, target, except, outdated).await,
Commands::Setup {
dry_run,
no_interactive,
} => setup::setup_shell(&mut app, using_env, dry_run, no_interactive).await,
Commands::Sync => sync::sync(&mut app).await,
Commands::Update { force, rc } => update::update_zv(&mut app, force, rc).await,
}
}
}
fn get_zv_lines() -> Vec<&'static str> {
vec![
"███████╗██╗ ██╗ ",
"╚══███╔╝██║ ██║ ",
" ███╔╝ ██║ ██║ ",
" ███╔╝ ██║ ██║ ",
"███████╗╚████╔╝█ ",
"╚══════╝ ╚══╝ ",
]
}
fn zv_line_with_color(line: &str, color: yansi::Color) -> String {
Paint::new(line).fg(color).to_string()
}
fn print_welcome_message(app: App) {
use target_lexicon::HOST;
let (color1, color2) = get_random_color_scheme();
let architecture = HOST.architecture;
let source_set = app.source_set;
let os = HOST.operating_system;
let zv_version = env!("CARGO_PKG_VERSION");
if tools::is_tty() {
let zv_lines = get_zv_lines();
let info_lines = vec![
format!("Architecture: {architecture}"),
format!("OS: {os}"),
format!(
"ZV status: {}",
if source_set {
Paint::green("✔ Ready to Use").to_string()
} else {
format!(
"{} {}",
Paint::red("Not in PATH."),
"Run ".to_string()
+ &Paint::blue("zv setup").to_string()
+ " to set ZV in PATH & install a default Zig version"
)
}
),
format!("ZV directory: {}", app.path().display().yellow()),
format!("ZV Version: {}", zv_version.yellow()),
format!(
"Shell: {}",
app.shell.as_ref().map_or(Shell::detect(), |s| s.clone())
),
];
let mut all_info_lines = info_lines;
if let Some(profile) = std::env::var("PROFILE").ok().filter(|p| !p.is_empty()) {
all_info_lines.push(format!("Profile: {profile}"));
}
println!();
for (i, zv_line) in zv_lines.iter().enumerate() {
let colored_line = if i < zv_lines.len() / 2 {
zv_line_with_color(zv_line, color1)
} else {
zv_line_with_color(zv_line, color2)
};
let info_part = if i < all_info_lines.len() {
format!(" {}", all_info_lines[i])
} else {
String::new()
};
println!("{}{}", colored_line, info_part);
}
for remaining_info in all_info_lines.iter().skip(zv_lines.len()) {
println!(" {}", remaining_info);
}
println!();
} else {
println!("zv - Zig Version Manager");
println!("Architecture: {architecture}");
println!("OS: {os}");
println!(
"ZV Setup: {}",
if source_set {
"Ready to Use"
} else {
"Not in PATH"
}
);
println!("ZV directory: {}", app.path().display());
println!(
"Shell: {}",
app.shell.as_ref().map_or(Shell::detect(), |s| s.clone())
);
if let Some(profile) = std::env::var("PROFILE").ok().filter(|p| !p.is_empty()) {
println!("Profile: {profile}");
}
println!("ZV Version: {}", zv_version);
println!();
}
let active_zig: Option<ZigVersion> = app.get_active_version();
let active_zig_str = active_zig
.as_ref()
.map_or_else(|| "none".to_string(), |v| v.to_string());
let help_text = if active_zig.is_none() {
format!(
" (use {} to set one | or run {} to get started)",
Paint::blue("zv use <version>"),
Paint::blue("zv setup")
)
} else {
String::new()
};
println!(
"Current active Zig: {}{}",
Paint::yellow(&active_zig_str),
help_text
);
if cfg!(windows) {
println!("{}", Paint::cyan("Usage: zv.exe [COMMAND]"));
} else {
println!("{}", Paint::cyan("Usage: zv [COMMAND]"));
}
println!();
println!("{}", Paint::yellow("Commands:").bold());
let print_command = |cmd: &str, desc: &str| {
println!("\t{:<12}\t{}", Paint::green(cmd), desc);
};
print_command(
"init",
"Initialize a new Zig project from lean or standard zig template",
);
print_command(
"install",
"Install Zig version(s) without setting as active",
);
print_command(
"use",
"Select which Zig version to use - master | latest | stable | <semver>",
);
print_command("list | ls", "List installed Zig versions");
print_command(
"clean | rm",
"Clean up Zig installations. Non-zv managed installations will not be affected",
);
print_command(
"setup",
"Setup shell environment for zv with interactive prompts (use --no-interactive to disable)",
);
print_command(
"sync",
"Synchronize index, mirrors list and metadata for zv",
);
print_command(
"help",
"Print this message or the help of the given subcommand(s)",
);
}
fn get_random_color_scheme() -> (yansi::Color, yansi::Color) {
use rand::Rng;
let schemes = [
(
yansi::Color::Rgb(255, 100, 0), yansi::Color::Rgb(0, 191, 255), ), (
yansi::Color::Rgb(255, 215, 0), yansi::Color::Rgb(75, 0, 130), ), (
yansi::Color::Rgb(220, 20, 60), yansi::Color::Rgb(0, 255, 255), ), (
yansi::Color::Rgb(247, 147, 26),
yansi::Color::Rgb(255, 255, 255),
), ];
let mut rng = rand::rng();
schemes[rng.random_range(0..schemes.len())]
}