free_launch/
launch_entry.rs

1use chrono::Local;
2use color_eyre::eyre::{OptionExt, Result};
3use egui::{Image, Ui};
4use std::fmt;
5use std::fs::create_dir_all;
6use std::path::Path;
7use tracing::{error, info, warn};
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) -> String;
16    fn file_path(&self) -> &Path;
17    fn icon_name(&self) -> Option<&str>;
18    fn comment(&self) -> Option<&str>;
19    fn debug_ui(&self, ui: &mut Ui, count: usize);
20}
21
22pub trait LaunchAction {
23    fn launch(&self);
24    fn open_directory(&self);
25}
26
27pub trait Launchable: LaunchId + LaunchAction + Send + Sync {}
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    /// Determine whether this entry is vaild for the display and launch of associated items
66    pub fn is_valid(&self) -> bool {
67        !self.id.is_empty() && !self.items.is_empty()
68    }
69
70    /// A `usize` to be used for sorting based on frecency
71    ///
72    /// This creates a value that is a combination of how recently something was invoked as well as how many times it was invoked.
73    /// It may need more tweaking for the best user experience.
74    pub fn sort_key(&self) -> u64 {
75        let now = Local::now().timestamp() as u64;
76        self.invocations
77            .iter()
78            .fold(0, |acc, i| acc + (i.timestamp / (now - i.timestamp).max(1)))
79    }
80
81    pub(crate) fn toggle_selected(&mut self) {
82        self.selected = !self.selected
83    }
84
85    // TODO return an option to prevent crashes
86    pub(crate) fn preferred_item(&self) -> Option<&Box<dyn Launchable>> {
87        self.items.first()
88    }
89
90    pub(crate) fn id(&self) -> Option<String> {
91        self.preferred_item().map(|i| i.id())
92    }
93
94    pub(crate) fn file_path(&self) -> Option<&Path> {
95        self.preferred_item().map(|i| i.file_path())
96    }
97
98    pub(crate) fn comment(&self) -> Option<&str> {
99        self.preferred_item().and_then(|i| i.comment())
100    }
101
102    pub(crate) fn launch(&self, search_query: &str) -> Result<()> {
103        info!("Launching entry: {}", self.name());
104
105        self.log_invocation("launch", search_query);
106
107        self.preferred_item()
108            .map(|i| i.launch())
109            .ok_or_eyre("could not launch item")
110    }
111
112    pub(crate) fn open_directory(&self, search_query: &str) -> Result<()> {
113        self.log_invocation("reveal", search_query);
114
115        self.preferred_item()
116            .map(|i| i.open_directory())
117            .ok_or_eyre("could not reveal item")
118    }
119
120    fn log_invocation(&self, action: &str, search_query: &str) {
121        // TODO make this a lazy loaded static
122        let cache_dir = PROJECT_DIRS.cache_dir();
123
124        // Create cache directory if it doesn't exist
125        if let Err(e) = create_dir_all(cache_dir) {
126            error!(
127                "Failed to create cache directory {}: {}",
128                cache_dir.display(),
129                e
130            );
131            return;
132        }
133
134        // Write to log file
135        // TODO do we need to make the appends atomic?
136        // TODO handle errors rather than ignoring them
137        if let Err(e) = Invocation::log_entry(search_query, action.to_owned(), self) {
138            warn!("Could not log invocation:{}", e)
139        }
140    }
141}
142
143impl fmt::Display for LaunchEntry {
144    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145        write!(f, "{}", self.name)
146    }
147}