free_launch/
launch_entry.rs

1use chrono::Local;
2use color_eyre::eyre::{OptionExt, Result};
3use egui::Image;
4use icon::IconFile;
5use std::fs::create_dir_all;
6use std::path::Path;
7use std::time::Instant;
8
9use crate::free_launch::PROJECT_DIRS;
10use crate::invocation::Invocation;
11
12/// Trait for items that can be launched and have an identifier
13// TODO rename to LaunchInfo
14pub trait LaunchId {
15    fn id(&self) -> &str;
16    fn file_path(&self) -> &Path;
17    fn icon_file(&self) -> Option<&IconFile>;
18    fn icon(&self) -> Option<Image>;
19    fn comment(&self) -> Option<&str>;
20}
21
22pub trait LaunchAction {
23    fn launch(&self);
24    fn open_directory(&self);
25}
26
27pub trait Launchable: LaunchId + LaunchAction {}
28
29/// Represents an item that can be launched in the item list
30pub struct LaunchEntry {
31    pub id: String,
32    pub name: String,
33    pub selected: bool,
34    pub items: Vec<Box<dyn Launchable>>,
35    // TODO consider changing to Rc<Invocation>
36    pub invocations: Vec<Invocation>,
37}
38
39impl LaunchEntry {
40    /// Create a new LaunchEntry with the given id
41    pub fn new(id: String, name: String) -> Self {
42        Self {
43            id,
44            name,
45            selected: false,
46            items: Vec::new(),
47            invocations: Vec::new(),
48        }
49    }
50
51    pub fn name(&self) -> &str {
52        &self.name
53    }
54
55    /// Add an item to this launch entry
56    pub fn add_item(&mut self, item: Box<dyn Launchable>) {
57        self.items.push(item);
58    }
59
60    /// Add an invocation to this launch entry
61    pub fn add_invocation(&mut self, invocation: Invocation) {
62        self.invocations.push(invocation);
63    }
64
65    /// A `usize` to be used for sorting based on frecency
66    ///
67    /// This creates a value that is a combination of how recently something was invoked as well as how many times it was invoked.
68    /// It may need more tweaking for the best user experience.
69    pub fn sort_key(&self) -> u64 {
70        let now = Local::now().timestamp() as u64;
71        let invocations = self.invocations.len().max(1);
72        self.invocations
73            .iter()
74            .fold(0, |acc, i| acc + (i.timestamp / (now - i.timestamp).max(1)))
75    }
76
77    pub(crate) fn toggle_selected(&mut self) {
78        self.selected = !self.selected
79    }
80
81    // TODO return an option to prevent crashes
82    pub(crate) fn preferred_item(&self) -> Option<&Box<dyn Launchable>> {
83        self.items.first()
84    }
85
86    pub(crate) fn id(&self) -> Option<&str> {
87        self.preferred_item().map(|i| i.id())
88    }
89
90    pub(crate) fn file_path(&self) -> Option<&Path> {
91        self.preferred_item().map(|i| i.file_path())
92    }
93
94    pub(crate) fn icon(&self) -> Option<Image> {
95        self.preferred_item().map(|i| i.icon()).flatten()
96    }
97
98    pub(crate) fn comment(&self) -> Option<&str> {
99        self.preferred_item().map(|i| i.comment()).flatten()
100    }
101
102    pub(crate) fn launch(&self, search_query: &str) -> Result<()> {
103        self.log_invocation("launch", search_query);
104
105        self.preferred_item()
106            .map(|i| i.launch())
107            .ok_or_eyre("could not launch item")
108    }
109
110    pub(crate) fn open_directory(&self, search_query: &str) -> Result<()> {
111        self.log_invocation("reveal", search_query);
112
113        self.preferred_item()
114            .map(|i| i.open_directory())
115            .ok_or_eyre("could not reveal item")
116    }
117
118    fn log_invocation(&self, action: &str, search_query: &str) {
119        // TODO make this a lazy loaded static
120        let cache_dir = PROJECT_DIRS.cache_dir();
121
122        // Create cache directory if it doesn't exist
123        if let Err(e) = create_dir_all(&cache_dir) {
124            eprintln!(
125                "Failed to create cache directory {}: {}",
126                cache_dir.display(),
127                e
128            );
129            return;
130        }
131
132        // Write to log file
133        // TODO do we need to make the appends atomic?
134        // TODO handle errors rather than ignoring them
135        Invocation::log_entry(search_query, action.to_owned(), self);
136    }
137}