Skip to main content

free_launch/launch_entries/
launch_entry.rs

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