Skip to main content

free_launch/launch_entries/
launch_entry.rs

1use chrono::Local;
2use niri_ipc::{Timestamp, Window as NiriWindow};
3use std::fmt;
4use std::fs::create_dir_all;
5use std::path::Path;
6use tracing::{error, info, warn};
7
8use crate::free_launch::free_launch::PROJECT_DIRS;
9use crate::launch_entries::action_name::ActionName;
10use crate::launch_entries::action_result::ActionResult;
11use crate::launch_entries::launch_entry_trait::Launchable;
12use crate::model::invocation::Invocation;
13use crate::model::window::Window;
14
15/// Represents an item that can be launched in the item list
16pub struct LaunchEntry {
17    pub id: String,
18    pub name: String,
19    pub selected: bool,
20    pub(crate) items: Vec<Box<dyn Launchable>>,
21    // TODO consider changing to Rc<Invocation>
22    pub invocations: Vec<Invocation>,
23    /// Activation order for sorting (higher = more recently activated)
24    /// Used primarily for window entries to sort by focus history
25    pub activation_order: Option<Timestamp>,
26}
27
28impl LaunchEntry {
29    /// Create a new LaunchEntry with the given id
30    pub fn new(id: String, name: String) -> Self {
31        Self {
32            id,
33            name,
34            selected: false,
35            items: Vec::new(),
36            invocations: Vec::new(),
37            activation_order: None,
38        }
39    }
40
41    pub fn name(&self) -> &str {
42        &self.name
43    }
44
45    /// Add an item to this launch entry
46    pub(crate) fn add_item(&mut self, item: Box<dyn Launchable>) {
47        self.items.push(item);
48    }
49
50    /// Add an invocation to this launch entry
51    pub(crate) fn add_invocation(&mut self, invocation: Invocation) {
52        self.invocations.push(invocation);
53    }
54
55    /// Determine whether this entry is vaild for the display and launch of associated items
56    pub fn is_valid(&self) -> bool {
57        !self.id.is_empty() && !self.items.is_empty()
58    }
59
60    /// A `usize` to be used for sorting based on frecency
61    ///
62    /// This creates a value that is a combination of how recently something was invoked as well as how many times it was invoked.
63    /// It may need more tweaking for the best user experience.
64    pub fn sort_key(&self) -> u64 {
65        let now = Local::now().timestamp() as u64;
66        self.invocations
67            .iter()
68            .fold(0, |acc, i| acc + (i.timestamp / (now - i.timestamp).max(1)))
69    }
70
71    pub(crate) fn toggle_selected(&mut self) {
72        self.selected = !self.selected
73    }
74
75    // TODO return an option to prevent crashes
76    pub(crate) fn preferred_item(&self) -> Option<&Box<dyn Launchable>> {
77        self.items.first()
78    }
79
80    pub(crate) fn id(&self) -> Option<String> {
81        self.preferred_item().map(|i| i.id())
82    }
83
84    pub(crate) fn file_path(&self) -> Option<&Path> {
85        self.preferred_item().map(|i| i.file_path())
86    }
87
88    pub(crate) fn comment(&self) -> Option<&str> {
89        self.preferred_item().and_then(|i| i.comment())
90    }
91
92    pub(crate) fn icon_name(&self) -> Option<&str> {
93        self.preferred_item().and_then(|i| i.icon_name())
94    }
95
96    pub(crate) fn invoke(&self, action: &ActionName, search_query: &str) -> ActionResult {
97        info!("Launching entry: {}", self.name());
98
99        match self.preferred_item().map(|i| i.invoke(action)) {
100            Some(result) => {
101                // TODO log the ActionResult as well
102                self.log_invocation(action, search_query, &result);
103                result
104            }
105            None => ActionResult::Error("could not find a preferred item".to_owned()),
106        }
107    }
108
109    pub(crate) fn invoke_default(&self, search_query: &str) -> ActionResult {
110        self.invoke(&ActionName::Default, search_query)
111    }
112
113    pub(crate) fn invoke_secondary(&self, search_query: &str) -> ActionResult {
114        match self.preferred_item() {
115            Some(item) => {
116                let secondary_action_name = item.secondary_action();
117                self.invoke(
118                    &ActionName::Action(secondary_action_name.to_owned()),
119                    search_query,
120                )
121            }
122            None => ActionResult::Error("could not find a preferred item".to_owned()),
123        }
124    }
125
126    fn log_invocation(&self, action: &ActionName, search_query: &str, result: &ActionResult) {
127        // TODO make this a lazy loaded static
128        let cache_dir = PROJECT_DIRS.cache_dir();
129
130        // Create cache directory if it doesn't exist
131        if let Err(e) = create_dir_all(cache_dir) {
132            error!(
133                "Failed to create cache directory {}: {}",
134                cache_dir.display(),
135                e
136            );
137            return;
138        }
139
140        // Write to log file
141        // TODO do we need to make the appends atomic?
142        // TODO handle errors rather than ignoring them
143        if let Err(e) = Invocation::log_entry(search_query, action.to_string(), self) {
144            warn!("Could not log invocation:{}", e)
145        }
146    }
147}
148
149impl From<Window> for LaunchEntry {
150    fn from(window: Window) -> Self {
151        let id = window.id();
152        let name = window.name().to_owned();
153        let activation_order = window.focus_timestamp();
154
155        // TODO get rid of the unwrap_or
156        let mut launch_entry = LaunchEntry::new(id, name.unwrap_or("UNNAMED".to_owned()));
157        launch_entry.activation_order = activation_order;
158        launch_entry.add_item(Box::new(window));
159        launch_entry
160    }
161}
162
163impl From<NiriWindow> for LaunchEntry {
164    fn from(niri_window: NiriWindow) -> Self {
165        let window: Window = niri_window.into();
166        window.into()
167    }
168}
169
170impl fmt::Display for LaunchEntry {
171    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172        write!(f, "{}", self.name)
173    }
174}
175
176// TODO implement the from trait for NiriWindow