use std::fmt::Display;
use std::fs;
use std::io::{Write, stdin, stdout};
use std::path::{Path, PathBuf};
use std::process::Command;
use anyhow::anyhow;
use colored::Colorize;
use crossterm::tty::IsTty;
use crate::pkg::types::Scope;
pub fn print_info<T: Display>(key: &str, value: T) {
println!("{key}: {value}");
}
pub fn format_version_summary(
branch: &str,
status: &str,
number: &str
) -> String {
let branch_short = if branch == "Production" {
"Prod."
} else if branch == "Development" {
"Dev."
} else if branch == "Public" {
"Pub."
} else if branch == "Special" {
"Spec."
} else {
branch
};
format!(
"{} {} {}",
branch_short.blue().bold().italic(),
status,
number,
)
}
pub fn format_version_full(
branch: &str,
status: &str,
number: &str,
commit: &str
) -> String {
format!(
"{} {}",
format_version_summary(branch, status, number),
commit.green()
)
}
pub fn print_aligned_info(key: &str, value: &str) {
let key_with_colon = format!("{key}:");
println!("{:<18}{}", key_with_colon.cyan(), value);
}
pub fn print_repo_warning(repo_name: &str) {
if crate::pkg::utils::is_mini_mode() {
if let Ok(index) = crate::pkg::mini_resolve::fetch_registry_index()
&& let Some(pkg_info) =
index.packages.values().find(|p| p.repo == repo_name)
{
let warning_message = match pkg_info.repo_type.as_str() {
"unofficial" => Some(
"This package is from an unofficial repository and is not \
trusted."
),
"community" => Some(
"This package is from a community repository. Use with \
caution."
),
"test" => Some(
"This package is from a testing repository and may not \
function correctly."
),
"archive" => Some(
"This package is from an archive repository and is no \
longer maintained."
),
_ => None
};
if let Some(message) = warning_message {
println!("\n{}: {}", "NOTE".yellow().bold(), message.yellow());
}
}
return;
}
if let Ok(db_path) = crate::pkg::resolve::get_db_root()
&& let Ok(repo_config) = crate::pkg::config::read_repo_config(&db_path)
{
let major_repo = repo_name.split('/').next().unwrap_or_default();
if let Some(repo_entry) =
repo_config.repos.iter().find(|r| r.name == major_repo)
{
let warning_message = match repo_entry.repo_type.as_str() {
"unofficial" => Some(
"This package is from an unofficial repository and is not \
trusted."
),
"community" => Some(
"This package is from a community repository. Use with \
caution."
),
"test" => Some(
"This package is from a testing repository and may not \
function correctly."
),
"archive" => Some(
"This package is from an archive repository and is no \
longer maintained."
),
_ => None
};
if let Some(message) = warning_message {
println!("\n{}: {}", "NOTE".yellow().bold(), message.yellow());
}
}
}
}
pub fn get_all_packages_for_completion() -> Vec<PackageCompletion> {
let mut completions = Vec::new();
let Ok(config) = crate::pkg::config::read_config() else {
return completions;
};
let mut registries = Vec::new();
if let Some(default) = &config.default_registry {
registries.push(default.handle.clone());
}
for reg in &config.added_registries {
registries.push(reg.handle.clone());
}
let default_handle = config.default_registry.as_ref().map(|r| &r.handle);
for handle in registries {
if handle.is_empty() {
continue;
}
if let Ok(entries) =
crate::pkg::db::get_packages_for_completion(&handle)
{
let is_default = default_handle == Some(&handle);
for entry in entries {
let base_name = if is_default {
format!("@{}/{}", entry.repo, entry.name)
} else {
format!("#{}@{}/{}", handle, entry.repo, entry.name)
};
let display = if let Some(sub) = entry.sub_package {
format!("{base_name}:{sub}")
} else {
base_name
};
completions.push(PackageCompletion {
display,
repo: entry.repo,
description: entry.description
});
}
}
}
completions.sort_by(|a, b| a.display.cmp(&b.display));
completions
}
pub struct PackageCompletion {
pub display: String,
pub repo: String,
pub description: String
}
pub fn symlink_file(target: &Path, link: &Path) -> std::io::Result<()> {
if link.exists() || link.is_symlink() {
fs::remove_file(link)?;
}
#[cfg(unix)]
{
std::os::unix::fs::symlink(target, link)
}
#[cfg(windows)]
{
if std::os::windows::fs::symlink_file(target, link).is_err() {
if fs::hard_link(target, link).is_err() {
fs::copy(target, link)?;
}
}
Ok(())
}
}
pub fn is_admin() -> bool {
#[cfg(windows)]
{
use std::{mem, ptr};
use winapi::um::handleapi::CloseHandle;
use winapi::um::processthreadsapi::{
GetCurrentProcess, OpenProcessToken
};
use winapi::um::securitybaseapi::CheckTokenMembership;
use winapi::um::winnt::{PSID, TOKEN_QUERY};
let mut token = ptr::null_mut();
let process = unsafe { GetCurrentProcess() };
if unsafe { OpenProcessToken(process, TOKEN_QUERY, &mut token) } == 0 {
return false;
}
let mut sid: [u8; 8] = [0; 8];
let mut sid_size = mem::size_of_val(&sid) as u32;
if unsafe {
winapi::um::securitybaseapi::CreateWellKnownSid(
winapi::um::winnt::WinBuiltinAdministratorsSid,
ptr::null_mut(),
sid.as_mut_ptr() as PSID,
&mut sid_size
)
} == 0
{
unsafe { CloseHandle(token) };
return false;
}
let mut is_member = 0;
let result = unsafe {
CheckTokenMembership(
token,
sid.as_mut_ptr() as PSID,
&mut is_member
)
};
unsafe { CloseHandle(token) };
result != 0 && is_member != 0
}
#[cfg(unix)]
{
nix::unistd::getuid().is_root()
}
}
pub fn check_license(license: &str) {
if license.is_empty() {
println!(
"{} Package does not have a license specified.",
"Warning:".yellow()
);
return;
}
if license.eq_ignore_ascii_case("None") {
println!(
"{} Package does not provide a license.",
"Warning:".yellow()
);
return;
}
if license.eq_ignore_ascii_case("Proprietary") {
println!(
"{} Package is using a proprietary license.",
"Warning:".red()
);
return;
}
if license.eq_ignore_ascii_case("Unknown") {
println!("{} Package license is unknown.", "Warning:".red());
return;
}
match spdx::Expression::parse(license) {
Ok(expr) => {
if !expr.evaluate(|req| match req.license {
spdx::LicenseItem::Spdx { id, .. } => id.is_osi_approved(),
spdx::LicenseItem::Other { .. } => false
}) {
println!(
"{} License expression '{}' does not evaluate to an OSI \
approved license.",
"Warning:".yellow(),
license.yellow().bold()
);
}
}
Err(_) => {
println!(
"{} Could not parse license expression '{}'. It may not be a \
valid SPDX identifier.",
"Warning:".yellow(),
license.yellow().bold()
);
}
}
}
pub fn ask_for_confirmation(prompt: &str, yes: bool) -> bool {
if yes {
return true;
}
if std::env::var("ZOI_TEST").is_ok() || !stdin().is_tty() {
return false;
}
print!("{} [y/N]: ", prompt.yellow());
let _ = stdout().flush();
let mut input = String::new();
if stdin().read_line(&mut input).is_err() {
return false;
}
input.trim().eq_ignore_ascii_case("y")
}
pub fn setup_path(scope: Scope) -> anyhow::Result<()> {
if scope == Scope::Project {
return Ok(());
}
let zoi_bin_dir = match scope {
Scope::User => {
let home = crate::pkg::utils::get_user_home()
.ok_or_else(|| anyhow!("Could not find home directory."))?;
crate::pkg::sysroot::apply_sysroot(
home.join(".zoi").join("pkgs").join("bin")
)
}
Scope::System => {
if cfg!(target_os = "windows") {
crate::pkg::sysroot::apply_sysroot(PathBuf::from(
"C:\\ProgramData\\zoi\\pkgs\\bin"
))
} else {
crate::pkg::sysroot::apply_sysroot(PathBuf::from(
"/usr/local/bin"
))
}
}
Scope::Project => return Ok(())
};
if !zoi_bin_dir.exists() {
fs::create_dir_all(&zoi_bin_dir)?;
}
if scope == Scope::System && cfg!(unix) {
println!(
"{}",
"System-wide installation complete. Binaries are in the system \
PATH."
.green()
);
return Ok(());
}
#[cfg(unix)]
{
use std::fs::{File, OpenOptions};
let home = crate::pkg::utils::get_user_home()
.ok_or_else(|| anyhow!("Could not find home directory."))?;
let zoi_bin_str = "$HOME/.zoi/pkgs/bin";
let shell_name = std::env::var("SHELL").unwrap_or_default();
let (profile_file_path, cmd_to_write) = if shell_name.contains("bash") {
let path = if cfg!(target_os = "macos") {
home.join(".bash_profile")
} else {
home.join(".bashrc")
};
let cmd = format!(
"\n# Added by Zoi\nexport PATH=\"{}:{}\"\n",
zoi_bin_str, "$PATH"
);
(path, cmd)
} else if shell_name.contains("zsh") {
let path = home.join(".zshrc");
let cmd = format!(
"\n# Added by Zoi\nexport PATH=\"{}:{}\"\n",
zoi_bin_str, "$PATH"
);
(path, cmd)
} else if shell_name.contains("fish") {
let path = home.join(".config/fish/config.fish");
let cmd =
format!("\n# Added by Zoi\nfish_add_path \"{zoi_bin_str}\"\n");
(path, cmd)
} else if shell_name.contains("elvish") {
let path = home.join(".config/elvish/rc.elv");
let cmd = "
# Added by Zoi
set paths = [ ~/.zoi/pkgs/bin $paths... ]
"
.to_string();
(path, cmd)
} else if shell_name.contains("csh") || shell_name.contains("tcsh") {
let path = home.join(".cshrc");
let cmd = format!(
"\n# Added by Zoi\nsetenv PATH=\"{}:{}\"\n",
zoi_bin_str, "$PATH"
);
(path, cmd)
} else {
let path = home.join(".profile");
let cmd = format!(
"\n# Added by Zoi\nexport PATH=\"{}:{}\"\n",
zoi_bin_str, "$PATH"
);
(path, cmd)
};
if !profile_file_path.exists() {
if let Some(parent) = profile_file_path.parent() {
fs::create_dir_all(parent)?;
}
File::create(&profile_file_path)?;
}
let content = fs::read_to_string(&profile_file_path)?;
if content.contains(zoi_bin_str) {
println!("Zoi bin directory is already in your shell's config.");
return Ok(());
}
let mut file =
OpenOptions::new().append(true).open(&profile_file_path)?;
file.write_all(cmd_to_write.as_bytes())?;
println!(
"{} Zoi bin directory has been added to your PATH in '{}'.",
"Success:".green(),
profile_file_path.display()
);
println!(
"Please restart your shell or run `source {}` for the changes to \
take effect.",
profile_file_path.display()
);
}
#[cfg(windows)]
{
use winreg::RegKey;
use winreg::enums::*;
let zoi_bin_path_str = zoi_bin_dir
.to_str()
.ok_or_else(|| anyhow!("Invalid path string"))?;
let (root, subkey, scope_name) = if scope == Scope::System {
if !is_admin() {
return Err(anyhow!(
"Administrator privileges required to modify system PATH."
));
}
(
HKEY_LOCAL_MACHINE,
"System\\CurrentControlSet\\Control\\Session \
Manager\\Environment",
"system"
)
} else {
(HKEY_CURRENT_USER, "Environment", "user")
};
let key = RegKey::predef(root);
let env = key.open_subkey_with_flags(subkey, KEY_READ | KEY_WRITE)?;
let current_path: String = env.get_value("Path")?;
if current_path
.split(';')
.any(|p| p.eq_ignore_ascii_case(zoi_bin_path_str))
{
println!("Zoi bin directory is already in your PATH.");
return Ok(());
}
let new_path = if current_path.is_empty() {
zoi_bin_path_str.to_string()
} else {
format!("{};{}", current_path, zoi_bin_path_str)
};
env.set_value("Path", &new_path)?;
println!(
"{} Zoi bin directory has been added to your {} PATH environment \
variable.",
"Success:".green(),
scope_name
);
println!(
"Please restart your shell or log out and log back in for the \
changes to take effect."
);
}
Ok(())
}
pub fn check_path() {
if let Some(home) = crate::pkg::utils::get_user_home() {
let zoi_bin_dir =
crate::pkg::sysroot::apply_sysroot(home.join(".zoi/pkgs/bin"));
if !zoi_bin_dir.exists() {
return;
}
} else {
return;
}
let command_output = if cfg!(target_os = "windows") {
Command::new("pwsh")
.arg("-Command")
.arg("echo $env:Path")
.output()
} else {
Command::new("bash").arg("-c").arg("echo $PATH").output()
};
let is_in_path = match command_output {
Ok(output) => {
if output.status.success() {
let path_var = String::from_utf8_lossy(&output.stdout);
path_var.contains(".zoi/pkgs/bin")
} else {
false
}
}
Err(_) => false
};
if !is_in_path {
eprintln!(
"Please run 'zoi shell <shell>' or add it to your PATH manually \
for commands to be available."
);
}
}