free_launch/launch_entries/
launch_entry.rs1use 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
14pub struct LaunchEntry {
16 pub id: String,
17 pub name: String,
18 pub selected: bool,
19 pub items: Vec<Box<dyn Launchable>>,
20 pub invocations: Vec<Invocation>,
22 pub activation_order: Option<Timestamp>,
25}
26
27impl LaunchEntry {
28 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 pub fn add_item(&mut self, item: Box<dyn Launchable>) {
46 self.items.push(item);
47 }
48
49 pub fn add_invocation(&mut self, invocation: Invocation) {
51 self.invocations.push(invocation);
52 }
53
54 pub fn is_valid(&self) -> bool {
56 !self.id.is_empty() && !self.items.is_empty()
57 }
58
59 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 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 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 let cache_dir = PROJECT_DIRS.cache_dir();
113
114 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 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 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