use chrono::Local;
use std::collections::HashMap;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use crate::events::{TraceDefinition, TraceStatus};
#[derive(Debug, Clone)]
pub struct TraceConfig {
pub id: u32,
pub target: String, pub script: String, pub status: TraceStatus, pub binary_path: String, pub selected_index: Option<usize>, }
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SaveFilter {
All, Enabled, Disabled, }
#[derive(Debug)]
pub struct SaveResult {
pub filename: PathBuf,
pub saved_count: usize,
pub total_count: usize,
}
#[derive(Debug)]
pub struct LoadResult {
pub filename: PathBuf,
pub loaded_count: usize,
pub enabled_count: usize,
pub disabled_count: usize,
}
pub struct TracePersistence {
traces: HashMap<u32, TraceConfig>,
binary_path: Option<String>,
pid: Option<u32>,
}
impl Default for TracePersistence {
fn default() -> Self {
Self::new()
}
}
impl TracePersistence {
pub fn new() -> Self {
Self {
traces: HashMap::new(),
binary_path: None,
pid: None,
}
}
pub fn set_binary_path(&mut self, path: String) {
self.binary_path = Some(path);
}
pub fn set_pid(&mut self, pid: u32) {
self.pid = Some(pid);
}
pub fn add_trace(&mut self, config: TraceConfig) {
self.traces.insert(config.id, config);
}
pub fn remove_trace(&mut self, id: u32) -> Option<TraceConfig> {
self.traces.remove(&id)
}
pub fn update_trace_status(&mut self, id: u32, status: TraceStatus) {
if let Some(trace) = self.traces.get_mut(&id) {
trace.status = status;
}
}
pub fn get_filtered_traces(&self, filter: SaveFilter) -> Vec<&TraceConfig> {
self.traces
.values()
.filter(|t| match filter {
SaveFilter::All => true,
SaveFilter::Enabled => matches!(t.status, TraceStatus::Active),
SaveFilter::Disabled => matches!(t.status, TraceStatus::Disabled),
})
.collect()
}
pub fn save_traces(
&self,
filename: Option<&str>,
filter: SaveFilter,
) -> io::Result<SaveResult> {
let path = if let Some(name) = filename {
PathBuf::from(name)
} else {
self.generate_default_filename()
};
let traces = self.get_filtered_traces(filter);
if traces.is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"No traces to save",
));
}
let content = self.generate_save_content(&traces, filter);
fs::write(&path, content)?;
Ok(SaveResult {
filename: path,
saved_count: traces.len(),
total_count: self.traces.len(),
})
}
fn generate_default_filename(&self) -> PathBuf {
let timestamp = Local::now().format("%Y%m%d_%H%M%S");
let binary_name = self
.binary_path
.as_ref()
.and_then(|p| Path::new(p).file_name())
.and_then(|n| n.to_str())
.unwrap_or("program");
PathBuf::from(format!("traces_{binary_name}_{timestamp}.gs"))
}
fn generate_save_content(&self, traces: &[&TraceConfig], filter: SaveFilter) -> String {
let mut content = String::new();
content.push_str(&self.generate_header(traces.len(), filter));
content.push('\n');
for (idx, trace) in traces.iter().enumerate() {
if idx > 0 {
content.push('\n');
}
content.push_str(&self.generate_trace_section(trace));
}
content
}
fn generate_header(&self, trace_count: usize, filter: SaveFilter) -> String {
let mut header = String::new();
header.push_str("// GhostScope Trace Save File v1.0\n");
let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S");
header.push_str(&format!("// Generated: {timestamp}\n"));
if let Some(ref binary) = self.binary_path {
header.push_str(&format!("// Binary: {binary}\n"));
}
if let Some(pid) = self.pid {
header.push_str(&format!("// PID: {pid}\n"));
}
let filter_desc = match filter {
SaveFilter::All => "all",
SaveFilter::Enabled => "enabled only",
SaveFilter::Disabled => "disabled only",
};
header.push_str(&format!("// Filter: {filter_desc}\n"));
let enabled_count = self
.traces
.values()
.filter(|t| matches!(t.status, TraceStatus::Active))
.count();
let disabled_count = self
.traces
.values()
.filter(|t| matches!(t.status, TraceStatus::Disabled))
.count();
header.push_str(&format!(
"// Traces: {trace_count} total ({enabled_count} enabled, {disabled_count} disabled)\n"
));
header
}
fn generate_trace_section(&self, trace: &TraceConfig) -> String {
let mut section = String::new();
section.push_str("// ========================================\n");
let status_str = match trace.status {
TraceStatus::Active => "ENABLED",
TraceStatus::Disabled => "DISABLED",
TraceStatus::Failed => "FAILED",
};
section.push_str(&format!(
"// Trace {}: {} [{}]\n",
trace.id, trace.target, status_str
));
section.push_str(&format!("// Target: {}\n", trace.target));
section.push_str(&format!("// Status: {}\n", trace.status));
if let Some(idx) = trace.selected_index {
section.push_str(&format!("// Index: {idx}\n"));
}
section.push_str("// ========================================\n");
if matches!(trace.status, TraceStatus::Disabled) {
section.push_str("//@disabled\n");
}
section.push_str(&format!("trace {} {{\n", trace.target));
for line in trace.script.lines() {
section.push_str(" ");
section.push_str(line);
section.push('\n');
}
section.push_str("}\n");
section
}
pub fn parse_trace_file(content: &str) -> io::Result<Vec<TraceDefinition>> {
let mut traces = Vec::new();
let mut current_target: Option<String> = None;
let mut in_script = false;
let mut script_lines = Vec::new();
let mut pending_disabled = false;
let mut pending_index: Option<usize> = None;
let mut brace_depth: usize = 0;
for line in content.lines() {
let trimmed = line.trim();
if trimmed == "//@disabled" {
pending_disabled = true;
continue;
}
if let Some(rest) = trimmed.strip_prefix("// Index:") {
let val = rest.trim();
if let Ok(idx) = val.parse::<usize>() {
pending_index = Some(idx);
}
continue;
}
if trimmed.starts_with("trace ") && trimmed.ends_with(" {") {
let target = trimmed
.strip_prefix("trace ")
.and_then(|s| s.strip_suffix(" {"))
.unwrap_or("")
.to_string();
current_target = Some(target);
in_script = true;
script_lines.clear();
brace_depth = 1;
continue;
}
if in_script && trimmed == "}" && brace_depth == 1 {
if let Some(target) = current_target.take() {
let script = script_lines.join("\n");
traces.push(TraceDefinition {
target,
script,
enabled: !pending_disabled,
selected_index: pending_index,
});
pending_disabled = false;
pending_index = None;
}
in_script = false;
brace_depth = 0;
continue;
}
if in_script {
let script_line = if let Some(stripped) = line.strip_prefix(" ") {
stripped
} else {
line
};
script_lines.push(script_line.to_string());
let opens = script_line.chars().filter(|&c| c == '{').count();
let closes = script_line.chars().filter(|&c| c == '}').count();
brace_depth = brace_depth.saturating_add(opens).saturating_sub(closes);
}
}
Ok(traces)
}
pub fn load_traces_from_file(filename: &str) -> io::Result<Vec<TraceDefinition>> {
let content = fs::read_to_string(filename)?;
Self::parse_trace_file(&content)
}
}
pub trait CommandParser {
fn parse_save_traces_command(&self) -> Option<(Option<String>, SaveFilter)>;
}
impl CommandParser for str {
fn parse_save_traces_command(&self) -> Option<(Option<String>, SaveFilter)> {
let parts: Vec<&str> = self.split_whitespace().collect();
if parts.len() < 2 || parts[0] != "save" || parts[1] != "traces" {
return None;
}
match parts.len() {
2 => {
Some((None, SaveFilter::All))
}
3 => {
match parts[2] {
"enabled" => Some((None, SaveFilter::Enabled)),
"disabled" => Some((None, SaveFilter::Disabled)),
filename => Some((Some(filename.to_string()), SaveFilter::All)),
}
}
4 => {
let filter = match parts[2] {
"enabled" => SaveFilter::Enabled,
"disabled" => SaveFilter::Disabled,
_ => return None,
};
Some((Some(parts[3].to_string()), filter))
}
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_save_command() {
let (file, filter) = "save traces".parse_save_traces_command().unwrap();
assert_eq!(file, None);
assert_eq!(filter, SaveFilter::All);
let (file, filter) = "save traces session.gs"
.parse_save_traces_command()
.unwrap();
assert_eq!(file, Some("session.gs".to_string()));
assert_eq!(filter, SaveFilter::All);
let (file, filter) = "save traces enabled".parse_save_traces_command().unwrap();
assert_eq!(file, None);
assert_eq!(filter, SaveFilter::Enabled);
let (file, filter) = "save traces disabled debug.gs"
.parse_save_traces_command()
.unwrap();
assert_eq!(file, Some("debug.gs".to_string()));
assert_eq!(filter, SaveFilter::Disabled);
}
#[test]
fn test_parse_trace_file() {
let content = r#"// Header
//@disabled
trace main {
print "hello";
print "world";
}
trace foo {
print "foo";
}"#;
let traces = TracePersistence::parse_trace_file(content).unwrap();
assert_eq!(traces.len(), 2);
assert_eq!(traces[0].target, "main");
assert!(!traces[0].enabled); assert_eq!(traces[0].script, "print \"hello\";\nprint \"world\";");
assert_eq!(traces[1].target, "foo");
assert!(traces[1].enabled); assert_eq!(traces[1].script, "print \"foo\";");
}
}