use std::fs::OpenOptions;
use std::io::{self, Write};
use anyhow::Result;
use fs2::FileExt;
use crate::cli::args::HistoryCommands;
use crate::cli::formatter;
use crate::config::history::{read_all_records, resolve_history_path, HistoryRecord};
use crate::config::manager::ConfigManager;
pub async fn run(
config_mgr: &ConfigManager,
subcmd: Option<HistoryCommands>,
output_format: &str,
no_header: bool,
) -> Result<()> {
match subcmd {
None | Some(HistoryCommands::List { .. }) => {
let (limit, all) = match subcmd {
Some(HistoryCommands::List { limit, all }) => (limit, all),
_ => (50, false),
};
list(config_mgr, limit, all, output_format, no_header)
}
Some(HistoryCommands::Search { keyword }) => {
search(config_mgr, &keyword, output_format, no_header)
}
Some(HistoryCommands::Clear { force }) => clear(config_mgr, force),
}
}
fn records_to_json(records: &[HistoryRecord]) -> serde_json::Value {
serde_json::to_value(records).unwrap_or(serde_json::Value::Array(vec![]))
}
fn print_records(records: &[HistoryRecord], output_format: &str, no_header: bool) {
let json = records_to_json(records);
if records.is_empty() {
match output_format {
"json" => {
println!("[]");
return;
}
"yaml" => {
println!("[]");
return;
}
"csv" => return,
"text" => return,
_ => {
eprintln!("No history records found.");
return;
}
}
}
match output_format {
"json" => println!("{}", formatter::format_json(&json)),
"yaml" => println!("{}", formatter::format_yaml(&json)),
"csv" => {
println!("{}", formatter::format_csv(&json, no_header));
}
"text" => println!("{}", formatter::format_text(&json)),
_ => {
let columns = &["timestamp", "command", "command_type", "success"];
if let serde_json::Value::Array(ref arr) = json {
let refs: Vec<&serde_json::Value> = arr.iter().collect();
println!(
"{}",
formatter::format_table_with_opts(&refs, columns, no_header, None)
);
}
}
}
}
fn list(
config_mgr: &ConfigManager,
limit: usize,
all: bool,
output_format: &str,
no_header: bool,
) -> Result<()> {
let path = resolve_history_path(config_mgr);
let records = read_all_records(&path);
let mut display_records: Vec<HistoryRecord> = records.into_iter().rev().collect();
if !all {
display_records.truncate(limit);
}
print_records(&display_records, output_format, no_header);
Ok(())
}
fn search(
config_mgr: &ConfigManager,
keyword: &str,
output_format: &str,
no_header: bool,
) -> Result<()> {
let path = resolve_history_path(config_mgr);
let records = read_all_records(&path);
let keyword_lower = keyword.to_lowercase();
let matched: Vec<HistoryRecord> = records
.into_iter()
.rev()
.filter(|r| r.command.to_lowercase().contains(&keyword_lower))
.collect();
print_records(&matched, output_format, no_header);
Ok(())
}
fn clear(config_mgr: &ConfigManager, force: bool) -> Result<()> {
let path = resolve_history_path(config_mgr);
if !force {
eprint!("Are you sure you want to clear all history? [y/N] ");
io::stderr().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
if input.trim().to_lowercase() != "y" {
eprintln!("Cancelled.");
return Ok(());
}
}
if path.exists() {
let file = OpenOptions::new()
.write(true)
.truncate(false)
.open(&path)
.map_err(|e| anyhow::anyhow!("Failed to open history file: {}", e))?;
file.lock_exclusive()
.map_err(|e| anyhow::anyhow!("Failed to lock history file: {}", e))?;
file.set_len(0)
.map_err(|e| anyhow::anyhow!("Failed to truncate history file: {}", e))?;
let remove_result = std::fs::remove_file(&path);
let _ = file.unlock();
remove_result.map_err(|e| anyhow::anyhow!("Failed to remove history file: {}", e))?;
}
eprintln!("History cleared.");
Ok(())
}