use std::path::PathBuf;
use anyhow::{Context, Result};
use colored::Colorize;
#[derive(Debug, Clone)]
pub enum CrashCommand {
List,
Show {
file: PathBuf
},
Send {
file: PathBuf,
yes: bool
},
Clear {
yes: bool
}
}
#[derive(Debug, Clone)]
pub enum TelemetryCommand {
Status,
Enable,
Disable,
Crash(CrashCommand),
Flush
}
fn resolve_crash_file(file: &PathBuf) -> Result<PathBuf> {
if file.exists() {
return Ok(file.clone());
}
let dir = zoi_telemetry::crash::list_crashes();
for known in dir {
if known.ends_with(file)
|| known.file_name().is_some_and(|n| n == file.as_os_str())
{
return Ok(known);
}
}
Err(anyhow::anyhow!(
"Crash report not found: {}. Run 'zoi telemetry crash list' to see \
available reports.",
file.display()
))
}
pub fn run(cmd: TelemetryCommand) -> Result<()> {
match cmd {
TelemetryCommand::Status => {
let cfg = crate::pkg::config::read_config()?;
let status = if cfg.telemetry_enabled {
"Enabled".green()
} else {
"Disabled".yellow()
};
println!(
"{} telemetry is currently {}.",
"::".bold().blue(),
status
);
if cfg.telemetry_enabled {
let id = crate::pkg::telemetry::get_anonymous_id();
println!("Anonymous Client ID: {}", id.cyan());
println!(
"\nThank you for helping us improve Zoi! We collect \
minimal, anonymous data about:"
);
println!(
"- {} (OS, Arch, Distro, Shell)",
"Environment".bold()
);
println!("- {} (Action, Scope, Reason)", "Operations".bold());
println!(
"- {} (Name, Version, Repo, License)",
"Package Metadata".bold()
);
let crashes = crate::pkg::telemetry::crash::list_crashes();
if crashes.is_empty() {
println!(
"- {}: none stored locally",
"Crash Reports".bold()
);
} else {
println!(
"- {}: {} stored locally (see 'zoi telemetry crash \
list')",
"Crash Reports".bold(),
crashes.len()
);
}
} else {
println!(
"\nTelemetry is anonymous and helps us prioritize \
features and platforms."
);
println!("Run 'zoi telemetry enable' to help the project.");
println!(
"Note: crash reports are still saved locally to \
$XDG_STATE_HOME/zoi/crash/ but never uploaded while \
telemetry is disabled."
);
}
}
TelemetryCommand::Enable => {
let mut cfg = crate::pkg::config::read_user_config()?;
println!(
"{}",
"Notice: Enabling telemetry shares anonymous usage data to \
help improve Zoi."
.dimmed()
);
println!(
"{}",
"Success analytics contain no personal data or IP addresses. \
Failure reports may include error text with local paths."
.dimmed()
);
println!(
"{} {}",
"Full Privacy Policy:".dimmed(),
"https://zillowe.qzz.io/legal/privacy".cyan()
);
cfg.telemetry_enabled = true;
crate::pkg::config::write_user_config(&cfg)?;
println!("{} telemetry enabled", "Success:".green());
}
TelemetryCommand::Disable => {
let mut cfg = crate::pkg::config::read_user_config()?;
cfg.telemetry_enabled = false;
crate::pkg::config::write_user_config(&cfg)?;
println!("{} telemetry disabled", "Success:".green());
}
TelemetryCommand::Crash(crash_cmd) => run_crash(crash_cmd)?,
TelemetryCommand::Flush => {
let summary = crate::pkg::telemetry::queue::flush_queue();
println!(
"Telemetry flush: {} sent, {} failed, {} skipped.",
summary.sent, summary.failed, summary.skipped
);
}
}
Ok(())
}
fn run_crash(cmd: CrashCommand) -> Result<()> {
match cmd {
CrashCommand::List => {
let crashes = crate::pkg::telemetry::crash::list_crashes();
if crashes.is_empty() {
println!("No crash reports stored locally.");
println!(
"Crash reports are saved to $XDG_STATE_HOME/zoi/crash/ \
(default ~/.local/state/zoi/crash/) with a .zoicrash \
extension."
);
} else {
println!("Stored crash reports:");
for path in crashes {
println!(" {}", path.display());
}
}
}
CrashCommand::Show { file } => {
let path = resolve_crash_file(&file)?;
let contents =
std::fs::read_to_string(&path).with_context(|| {
format!("Failed to read {}", path.display())
})?;
println!("{contents}");
}
CrashCommand::Send { file, yes } => {
let path = resolve_crash_file(&file)?;
if !yes
&& !crate::pkg::utils::ask_for_confirmation(
&format!(
"Upload {} to Sentry? Crash reports may contain stack \
memory snapshots.",
path.display()
),
false
)
{
println!("Upload cancelled.");
return Ok(());
}
let sent = crate::pkg::telemetry::crash::send_crash_report(&path)
.map_err(|e| anyhow::anyhow!("{e}"))?;
if sent {
println!("{} crash report uploaded", "Success:".green());
std::fs::remove_file(&path).with_context(|| {
format!("Uploaded but failed to remove {}", path.display())
})?;
} else {
println!(
"{} telemetry is disabled; crash report was not uploaded. \
Enable with 'zoi telemetry enable' first.",
"Skipped:".yellow()
);
}
}
CrashCommand::Clear { yes } => {
let crashes = crate::pkg::telemetry::crash::list_crashes();
if crashes.is_empty() {
println!("No crash reports to delete.");
return Ok(());
}
if !yes
&& !crate::pkg::utils::ask_for_confirmation(
&format!(
"Delete {} stored crash report(s)?",
crashes.len()
),
false
)
{
println!("Cancelled.");
return Ok(());
}
for path in crashes {
std::fs::remove_file(&path).with_context(|| {
format!("Failed to remove {}", path.display())
})?;
}
println!("{} crash reports cleared", "Success:".green());
}
}
Ok(())
}