free_launch/
invocation.rs

1use chrono::Utc;
2use color_eyre::Result;
3use csv::WriterBuilder;
4use serde::{Deserialize, Serialize};
5use std::{
6    fs::{File, OpenOptions},
7    path::{Path, PathBuf},
8    time::SystemTime,
9};
10
11use crate::{free_launch::PROJECT_DIRS, launch_entry::LaunchEntry};
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct Invocation {
15    pub timestamp: u64,
16    pub search_query: String,
17    pub action: String,
18    pub item_name: String,
19    pub item_id: String,
20    pub item_path: String,
21}
22
23impl Invocation {
24    pub fn new(search_query: &str, action: String, launch_entry: &LaunchEntry) -> Self {
25        // Setup timestamp
26        // TODO make this more concise, this was originally generated by Claude and seems overly verbose
27        let now: SystemTime = SystemTime::now();
28        let timestamp = now
29            .duration_since(SystemTime::UNIX_EPOCH)
30            .unwrap_or_default()
31            .as_secs();
32
33        Self {
34            timestamp,
35            search_query: search_query.to_owned(),
36            action,
37            item_name: launch_entry.name().to_owned(),
38            item_id: launch_entry.id().map(|i| i.to_owned()).unwrap_or_default(),
39            item_path: launch_entry
40                .file_path()
41                .map(|i| i.to_string_lossy().to_string())
42                .unwrap_or_default(),
43        }
44    }
45
46    pub(crate) fn id(&self) -> &str {
47        &self.item_id
48    }
49
50    pub(crate) fn name(&self) -> &str {
51        &self.item_name
52    }
53
54    fn cache_file() -> PathBuf {
55        // Format date as YYYY-MM-DD
56        let date_str = Utc::now().format("%Y-%m-%d").to_string();
57        let log_filename = format!("free-launch-invocations-{}.tsv", date_str);
58        PROJECT_DIRS.cache_dir().join(log_filename)
59    }
60
61    pub(crate) fn log_entry(
62        search_query: &str,
63        action: String,
64        launch_entry: &LaunchEntry,
65    ) -> Result<()> {
66        Invocation::new(search_query, action, launch_entry).append_to_csv(&Self::cache_file())
67    }
68
69    fn append_to_csv(&self, file_path: &Path) -> Result<()> {
70        // Check if the file already exists so we don't write headers more than once
71        let write_headers = !file_path.exists();
72
73        // Open the file in append mode.
74        // If the file does not exist, create it.
75        let file = OpenOptions::new()
76            .append(true)
77            .create(true)
78            .open(file_path)?;
79
80        // Create a CSV writer that writes to the file.
81        // Note: When appending, you might want to avoid writing headers if they already exist.
82        let mut wtr = WriterBuilder::new()
83            .has_headers(write_headers)
84            .from_writer(file);
85
86        // Write each record in append mode
87        wtr.serialize(self)?;
88
89        // Make sure all data is flushed to the file
90        wtr.flush()?;
91        Ok(())
92    }
93
94    pub(crate) fn load_from_cache() -> Result<Vec<Invocation>> {
95        // Create a CSV reader builder
96        let mut rdr = csv::Reader::from_reader(File::open(Self::cache_file())?);
97        let mut records = Vec::new();
98
99        // Deserialize each record into our struct
100        for result in rdr.deserialize() {
101            let record: Invocation = result?;
102            records.push(record);
103        }
104
105        Ok(records)
106    }
107}