style: apply rustfmt across workspace
This commit is contained in:
@@ -107,8 +107,7 @@ impl Default for PluginsCfg {
|
||||
}
|
||||
|
||||
pub fn load() -> Config {
|
||||
let path = dirs::config_dir()
|
||||
.map(|d| d.join("k-launcher").join("config.toml"));
|
||||
let path = dirs::config_dir().map(|d| d.join("k-launcher").join("config.toml"));
|
||||
let Some(path) = path else {
|
||||
return Config::default();
|
||||
};
|
||||
|
||||
@@ -29,7 +29,9 @@ impl ResultTitle {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(
|
||||
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
|
||||
)]
|
||||
pub struct Score(u32);
|
||||
|
||||
impl Score {
|
||||
@@ -104,7 +106,10 @@ pub struct Kernel {
|
||||
|
||||
impl Kernel {
|
||||
pub fn new(plugins: Vec<Arc<dyn Plugin>>, max_results: usize) -> Self {
|
||||
Self { plugins, max_results }
|
||||
Self {
|
||||
plugins,
|
||||
max_results,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn search(&self, query: &str) -> Vec<SearchResult> {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::process::{Command, Stdio};
|
||||
use std::os::unix::process::CommandExt;
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
use k_launcher_kernel::{AppLauncher, LaunchAction};
|
||||
|
||||
@@ -77,21 +77,31 @@ impl AppLauncher for UnixAppLauncher {
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.pre_exec(|| { libc::setsid(); Ok(()) })
|
||||
.pre_exec(|| {
|
||||
libc::setsid();
|
||||
Ok(())
|
||||
})
|
||||
.spawn()
|
||||
};
|
||||
}
|
||||
}
|
||||
LaunchAction::SpawnInTerminal(cmd) => {
|
||||
let Some((term_bin, term_args)) = resolve_terminal() else { return };
|
||||
let Some((term_bin, term_args)) = resolve_terminal() else {
|
||||
return;
|
||||
};
|
||||
let _ = unsafe {
|
||||
Command::new(&term_bin)
|
||||
.args(&term_args)
|
||||
.arg("sh").arg("-c").arg(cmd)
|
||||
.arg("sh")
|
||||
.arg("-c")
|
||||
.arg(cmd)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.pre_exec(|| { libc::setsid(); Ok(()) })
|
||||
.pre_exec(|| {
|
||||
libc::setsid();
|
||||
Ok(())
|
||||
})
|
||||
.spawn()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -43,7 +43,9 @@ async fn do_search(
|
||||
io: &mut ProcessIo,
|
||||
query: &str,
|
||||
) -> Result<Vec<ExternalResult>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let line = serde_json::to_string(&Query { query: query.to_string() })?;
|
||||
let line = serde_json::to_string(&Query {
|
||||
query: query.to_string(),
|
||||
})?;
|
||||
io.stdin.write_all(line.as_bytes()).await?;
|
||||
io.stdin.write_all(b"\n").await?;
|
||||
io.stdin.flush().await?;
|
||||
@@ -143,7 +145,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn query_serializes_correctly() {
|
||||
let q = Query { query: "firefox".to_string() };
|
||||
let q = Query {
|
||||
query: "firefox".to_string(),
|
||||
};
|
||||
assert_eq!(serde_json::to_string(&q).unwrap(), r#"{"query":"firefox"}"#);
|
||||
}
|
||||
|
||||
@@ -155,21 +159,27 @@ mod tests {
|
||||
assert_eq!(results[0].id, "1");
|
||||
assert_eq!(results[0].title, "Firefox");
|
||||
assert_eq!(results[0].score, 80);
|
||||
assert!(matches!(&results[0].action, ExternalAction::SpawnProcess { cmd } if cmd == "firefox"));
|
||||
assert!(
|
||||
matches!(&results[0].action, ExternalAction::SpawnProcess { cmd } if cmd == "firefox")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn result_parses_copy_action() {
|
||||
let json = r#"[{"id":"c","title":"= 4","score":90,"action":{"type":"CopyToClipboard","text":"4"}}]"#;
|
||||
let results: Vec<ExternalResult> = serde_json::from_str(json).unwrap();
|
||||
assert!(matches!(&results[0].action, ExternalAction::CopyToClipboard { text } if text == "4"));
|
||||
assert!(
|
||||
matches!(&results[0].action, ExternalAction::CopyToClipboard { text } if text == "4")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn result_parses_open_path_action() {
|
||||
let json = r#"[{"id":"f","title":"/home/user","score":50,"action":{"type":"OpenPath","path":"/home/user"}}]"#;
|
||||
let results: Vec<ExternalResult> = serde_json::from_str(json).unwrap();
|
||||
assert!(matches!(&results[0].action, ExternalAction::OpenPath { path } if path == "/home/user"));
|
||||
assert!(
|
||||
matches!(&results[0].action, ExternalAction::OpenPath { path } if path == "/home/user")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -182,7 +192,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn result_parses_missing_optional_fields() {
|
||||
let json = r#"[{"id":"x","title":"X","score":10,"action":{"type":"SpawnProcess","cmd":"x"}}]"#;
|
||||
let json =
|
||||
r#"[{"id":"x","title":"X","score":10,"action":{"type":"SpawnProcess","cmd":"x"}}]"#;
|
||||
let results: Vec<ExternalResult> = serde_json::from_str(json).unwrap();
|
||||
assert!(results[0].description.is_none());
|
||||
assert!(results[0].icon.is_none());
|
||||
|
||||
@@ -127,11 +127,20 @@ impl eframe::App for KLauncherApp {
|
||||
ui.set_width(ui.available_width());
|
||||
for (i, result) in self.results.iter().enumerate() {
|
||||
let is_selected = i == self.selected;
|
||||
let bg = if is_selected { SELECTED_BG } else { Color32::TRANSPARENT };
|
||||
let bg = if is_selected {
|
||||
SELECTED_BG
|
||||
} else {
|
||||
Color32::TRANSPARENT
|
||||
};
|
||||
|
||||
let row_frame = egui::Frame::new()
|
||||
.fill(bg)
|
||||
.inner_margin(egui::Margin { left: 8, right: 8, top: 6, bottom: 6 })
|
||||
.inner_margin(egui::Margin {
|
||||
left: 8,
|
||||
right: 8,
|
||||
top: 6,
|
||||
bottom: 6,
|
||||
})
|
||||
.corner_radius(egui::CornerRadius::same(4));
|
||||
|
||||
row_frame.show(ui, |ui| {
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use iced::{
|
||||
Border, Color, Element, Length, Size, Subscription, Task,
|
||||
event,
|
||||
Border, Color, Element, Length, Size, Subscription, Task, event,
|
||||
keyboard::{Event as KeyEvent, Key, key::Named},
|
||||
widget::{column, container, image, row, scrollable, svg, text, text_input, Space},
|
||||
widget::{Space, column, container, image, row, scrollable, svg, text, text_input},
|
||||
window,
|
||||
};
|
||||
|
||||
@@ -126,8 +125,13 @@ fn view(state: &KLauncherApp) -> Element<'_, Message> {
|
||||
.padding(12)
|
||||
.size(cfg.search_font_size)
|
||||
.style(|theme, _status| {
|
||||
let mut s = iced::widget::text_input::default(theme, iced::widget::text_input::Status::Active);
|
||||
s.border = Border { color: Color::TRANSPARENT, width: 0.0, radius: 0.0.into() };
|
||||
let mut s =
|
||||
iced::widget::text_input::default(theme, iced::widget::text_input::Status::Active);
|
||||
s.border = Border {
|
||||
color: Color::TRANSPARENT,
|
||||
width: 0.0,
|
||||
radius: 0.0.into(),
|
||||
};
|
||||
s
|
||||
});
|
||||
|
||||
@@ -147,26 +151,27 @@ fn view(state: &KLauncherApp) -> Element<'_, Message> {
|
||||
Color::from_rgba8(255, 255, 255, 0.07)
|
||||
};
|
||||
let icon_el: Element<'_, Message> = match &result.icon {
|
||||
Some(p) if p.ends_with(".svg") =>
|
||||
svg(svg::Handle::from_path(p)).width(24).height(24).into(),
|
||||
Some(p) =>
|
||||
image(image::Handle::from_path(p)).width(24).height(24).into(),
|
||||
Some(p) if p.ends_with(".svg") => {
|
||||
svg(svg::Handle::from_path(p)).width(24).height(24).into()
|
||||
}
|
||||
Some(p) => image(image::Handle::from_path(p))
|
||||
.width(24)
|
||||
.height(24)
|
||||
.into(),
|
||||
None => Space::new().width(24).height(24).into(),
|
||||
};
|
||||
let title_col: Element<'_, Message> = if let Some(desc) = &result.description {
|
||||
column![
|
||||
text(result.title.as_str()).size(title_size),
|
||||
text(desc).size(desc_size).color(Color::from_rgba8(210, 215, 230, 1.0)),
|
||||
text(desc)
|
||||
.size(desc_size)
|
||||
.color(Color::from_rgba8(210, 215, 230, 1.0)),
|
||||
]
|
||||
.into()
|
||||
} else {
|
||||
text(result.title.as_str()).size(title_size).into()
|
||||
};
|
||||
container(
|
||||
row![icon_el, title_col]
|
||||
.spacing(8)
|
||||
.align_y(iced::Center),
|
||||
)
|
||||
container(row![icon_el, title_col].spacing(8).align_y(iced::Center))
|
||||
.width(Length::Fill)
|
||||
.padding([6, 12])
|
||||
.style(move |_theme| container::Style {
|
||||
@@ -209,7 +214,8 @@ fn view(state: &KLauncherApp) -> Element<'_, Message> {
|
||||
.into()
|
||||
});
|
||||
|
||||
let mut content_children: Vec<Element<'_, Message>> = vec![search_bar.into(), results_list.into()];
|
||||
let mut content_children: Vec<Element<'_, Message>> =
|
||||
vec![search_bar.into(), results_list.into()];
|
||||
if let Some(err) = maybe_error {
|
||||
content_children.push(err);
|
||||
}
|
||||
@@ -261,15 +267,15 @@ pub fn run(
|
||||
update,
|
||||
view,
|
||||
)
|
||||
.title("K-Launcher")
|
||||
.subscription(subscription)
|
||||
.window(window::Settings {
|
||||
size: Size::new(wc.width, wc.height),
|
||||
position: window::Position::Centered,
|
||||
decorations: wc.decorations,
|
||||
transparent: wc.transparent,
|
||||
resizable: wc.resizable,
|
||||
..Default::default()
|
||||
})
|
||||
.run()
|
||||
.title("K-Launcher")
|
||||
.subscription(subscription)
|
||||
.window(window::Settings {
|
||||
size: Size::new(wc.width, wc.height),
|
||||
position: window::Position::Centered,
|
||||
decorations: wc.decorations,
|
||||
transparent: wc.transparent,
|
||||
resizable: wc.resizable,
|
||||
..Default::default()
|
||||
})
|
||||
.run()
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::io::Write;
|
||||
|
||||
pub fn send_show() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let runtime_dir = std::env::var("XDG_RUNTIME_DIR")
|
||||
.unwrap_or_else(|_| "/run/user/1000".to_string());
|
||||
let runtime_dir =
|
||||
std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/run/user/1000".to_string());
|
||||
let socket_path = format!("{runtime_dir}/k-launcher.sock");
|
||||
let mut stream = std::os::unix::net::UnixStream::connect(&socket_path)?;
|
||||
stream.write_all(b"show\n")?;
|
||||
|
||||
@@ -5,9 +5,9 @@ use std::sync::Arc;
|
||||
use k_launcher_kernel::Kernel;
|
||||
use k_launcher_os_bridge::UnixAppLauncher;
|
||||
use k_launcher_plugin_host::ExternalPlugin;
|
||||
use plugin_apps::{AppsPlugin, frecency::FrecencyStore};
|
||||
#[cfg(target_os = "linux")]
|
||||
use plugin_apps::linux::FsDesktopEntrySource;
|
||||
use plugin_apps::{AppsPlugin, frecency::FrecencyStore};
|
||||
use plugin_calc::CalcPlugin;
|
||||
use plugin_cmd::CmdPlugin;
|
||||
use plugin_files::FilesPlugin;
|
||||
@@ -36,14 +36,27 @@ fn run_ui() -> iced::Result {
|
||||
let frecency = FrecencyStore::load();
|
||||
|
||||
let mut plugins: Vec<Arc<dyn k_launcher_kernel::Plugin>> = vec![];
|
||||
if cfg.plugins.cmd { plugins.push(Arc::new(CmdPlugin::new())); }
|
||||
if cfg.plugins.calc { plugins.push(Arc::new(CalcPlugin::new())); }
|
||||
if cfg.plugins.files { plugins.push(Arc::new(FilesPlugin::new())); }
|
||||
if cfg.plugins.apps {
|
||||
plugins.push(Arc::new(AppsPlugin::new(FsDesktopEntrySource::new(), frecency)));
|
||||
if cfg.plugins.cmd {
|
||||
plugins.push(Arc::new(CmdPlugin::new()));
|
||||
}
|
||||
if cfg.plugins.calc {
|
||||
plugins.push(Arc::new(CalcPlugin::new()));
|
||||
}
|
||||
if cfg.plugins.files {
|
||||
plugins.push(Arc::new(FilesPlugin::new()));
|
||||
}
|
||||
if cfg.plugins.apps {
|
||||
plugins.push(Arc::new(AppsPlugin::new(
|
||||
FsDesktopEntrySource::new(),
|
||||
frecency,
|
||||
)));
|
||||
}
|
||||
for ext in &cfg.plugins.external {
|
||||
plugins.push(Arc::new(ExternalPlugin::new(&ext.name, &ext.path, ext.args.clone())));
|
||||
plugins.push(Arc::new(ExternalPlugin::new(
|
||||
&ext.name,
|
||||
&ext.path,
|
||||
ext.args.clone(),
|
||||
)));
|
||||
}
|
||||
|
||||
let kernel: Arc<dyn k_launcher_kernel::SearchEngine> =
|
||||
|
||||
@@ -2,9 +2,9 @@ use std::sync::Arc;
|
||||
|
||||
use k_launcher_kernel::Kernel;
|
||||
use k_launcher_os_bridge::UnixAppLauncher;
|
||||
use plugin_apps::{AppsPlugin, frecency::FrecencyStore};
|
||||
#[cfg(target_os = "linux")]
|
||||
use plugin_apps::linux::FsDesktopEntrySource;
|
||||
use plugin_apps::{AppsPlugin, frecency::FrecencyStore};
|
||||
use plugin_calc::CalcPlugin;
|
||||
use plugin_cmd::CmdPlugin;
|
||||
use plugin_files::FilesPlugin;
|
||||
@@ -12,12 +12,15 @@ use plugin_files::FilesPlugin;
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let launcher = Arc::new(UnixAppLauncher::new());
|
||||
let frecency = FrecencyStore::load();
|
||||
let kernel: Arc<dyn k_launcher_kernel::SearchEngine> = Arc::new(Kernel::new(vec![
|
||||
Arc::new(CmdPlugin::new()),
|
||||
Arc::new(CalcPlugin::new()),
|
||||
Arc::new(FilesPlugin::new()),
|
||||
Arc::new(AppsPlugin::new(FsDesktopEntrySource::new(), frecency)),
|
||||
], 8));
|
||||
let kernel: Arc<dyn k_launcher_kernel::SearchEngine> = Arc::new(Kernel::new(
|
||||
vec![
|
||||
Arc::new(CmdPlugin::new()),
|
||||
Arc::new(CalcPlugin::new()),
|
||||
Arc::new(FilesPlugin::new()),
|
||||
Arc::new(AppsPlugin::new(FsDesktopEntrySource::new(), frecency)),
|
||||
],
|
||||
8,
|
||||
));
|
||||
k_launcher_ui_egui::run(kernel, launcher)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -24,7 +24,10 @@ impl FrecencyStore {
|
||||
.ok()
|
||||
.and_then(|s| serde_json::from_str(&s).ok())
|
||||
.unwrap_or_default();
|
||||
Arc::new(Self { path, data: Mutex::new(data) })
|
||||
Arc::new(Self {
|
||||
path,
|
||||
data: Mutex::new(data),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -53,7 +56,10 @@ impl FrecencyStore {
|
||||
.unwrap_or_default()
|
||||
.as_secs();
|
||||
let mut data = self.data.lock().unwrap();
|
||||
let entry = data.entry(id.to_string()).or_insert(Entry { count: 0, last_used: 0 });
|
||||
let entry = data.entry(id.to_string()).or_insert(Entry {
|
||||
count: 0,
|
||||
last_used: 0,
|
||||
});
|
||||
entry.count += 1;
|
||||
entry.last_used = now;
|
||||
if let Some(parent) = self.path.parent() {
|
||||
@@ -72,7 +78,13 @@ impl FrecencyStore {
|
||||
.unwrap_or_default()
|
||||
.as_secs();
|
||||
let age_secs = now.saturating_sub(entry.last_used);
|
||||
let decay = if age_secs < 3600 { 4 } else if age_secs < 86400 { 2 } else { 1 };
|
||||
let decay = if age_secs < 3600 {
|
||||
4
|
||||
} else if age_secs < 86400 {
|
||||
2
|
||||
} else {
|
||||
1
|
||||
};
|
||||
entry.count * decay
|
||||
}
|
||||
|
||||
@@ -86,7 +98,13 @@ impl FrecencyStore {
|
||||
.iter()
|
||||
.map(|(id, entry)| {
|
||||
let age_secs = now.saturating_sub(entry.last_used);
|
||||
let decay = if age_secs < 3600 { 4 } else if age_secs < 86400 { 2 } else { 1 };
|
||||
let decay = if age_secs < 3600 {
|
||||
4
|
||||
} else if age_secs < 86400 {
|
||||
2
|
||||
} else {
|
||||
1
|
||||
};
|
||||
(id.clone(), entry.count * decay)
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -91,7 +91,10 @@ impl AppsPlugin {
|
||||
let id = format!("app-{}", e.name.as_str());
|
||||
let keywords_lc = e.keywords.iter().map(|k| k.to_lowercase()).collect();
|
||||
#[cfg(target_os = "linux")]
|
||||
let icon = e.icon.as_ref().and_then(|p| linux::resolve_icon_path(p.as_str()));
|
||||
let icon = e
|
||||
.icon
|
||||
.as_ref()
|
||||
.and_then(|p| linux::resolve_icon_path(p.as_str()));
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let icon: Option<String> = None;
|
||||
let exec = e.exec.as_str().to_string();
|
||||
@@ -117,13 +120,16 @@ impl AppsPlugin {
|
||||
}
|
||||
|
||||
fn initials(name_lc: &str) -> String {
|
||||
name_lc.split_whitespace().filter_map(|w| w.chars().next()).collect()
|
||||
name_lc
|
||||
.split_whitespace()
|
||||
.filter_map(|w| w.chars().next())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn score_match(name: &str, query: &str) -> Option<u32> {
|
||||
use nucleo_matcher::{
|
||||
pattern::{CaseMatching, Normalization, Pattern},
|
||||
Config, Matcher, Utf32Str,
|
||||
pattern::{CaseMatching, Normalization, Pattern},
|
||||
};
|
||||
|
||||
let mut matcher = Matcher::new(Config::DEFAULT);
|
||||
@@ -136,7 +142,11 @@ fn score_match(name: &str, query: &str) -> Option<u32> {
|
||||
if let Some(s) = score {
|
||||
let name_lc = name.to_lowercase();
|
||||
let query_lc = query.to_lowercase();
|
||||
let bonus: u32 = if initials(&name_lc).starts_with(&query_lc) { 20 } else { 0 };
|
||||
let bonus: u32 = if initials(&name_lc).starts_with(&query_lc) {
|
||||
20
|
||||
} else {
|
||||
0
|
||||
};
|
||||
Some(s.saturating_add(bonus))
|
||||
} else {
|
||||
None
|
||||
@@ -162,7 +172,9 @@ impl Plugin for AppsPlugin {
|
||||
|
||||
async fn search(&self, query: &str) -> Vec<SearchResult> {
|
||||
if query.is_empty() {
|
||||
return self.frecency.top_ids(5)
|
||||
return self
|
||||
.frecency
|
||||
.top_ids(5)
|
||||
.iter()
|
||||
.filter_map(|id| {
|
||||
let e = self.entries.get(id)?;
|
||||
@@ -185,7 +197,10 @@ impl Plugin for AppsPlugin {
|
||||
.values()
|
||||
.filter_map(|e| {
|
||||
let score = score_match(e.name.as_str(), query).or_else(|| {
|
||||
e.keywords_lc.iter().any(|k| k.contains(&query_lc)).then_some(50)
|
||||
e.keywords_lc
|
||||
.iter()
|
||||
.any(|k| k.contains(&query_lc))
|
||||
.then_some(50)
|
||||
})?;
|
||||
Some(SearchResult {
|
||||
id: ResultId::new(&e.id),
|
||||
@@ -238,7 +253,14 @@ mod tests {
|
||||
Self {
|
||||
entries: entries
|
||||
.into_iter()
|
||||
.map(|(n, e, kw)| (n.to_string(), e.to_string(), None, kw.into_iter().map(|s| s.to_string()).collect()))
|
||||
.map(|(n, e, kw)| {
|
||||
(
|
||||
n.to_string(),
|
||||
e.to_string(),
|
||||
None,
|
||||
kw.into_iter().map(|s| s.to_string()).collect(),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
@@ -261,20 +283,29 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn apps_prefix_match() {
|
||||
let p = AppsPlugin::new(MockSource::with(vec![("Firefox", "firefox")]), ephemeral_frecency());
|
||||
let p = AppsPlugin::new(
|
||||
MockSource::with(vec![("Firefox", "firefox")]),
|
||||
ephemeral_frecency(),
|
||||
);
|
||||
let results = p.search("fire").await;
|
||||
assert_eq!(results[0].title.as_str(), "Firefox");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn apps_no_match_returns_empty() {
|
||||
let p = AppsPlugin::new(MockSource::with(vec![("Firefox", "firefox")]), ephemeral_frecency());
|
||||
let p = AppsPlugin::new(
|
||||
MockSource::with(vec![("Firefox", "firefox")]),
|
||||
ephemeral_frecency(),
|
||||
);
|
||||
assert!(p.search("zz").await.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn apps_empty_query_no_frecency_returns_empty() {
|
||||
let p = AppsPlugin::new(MockSource::with(vec![("Firefox", "firefox")]), ephemeral_frecency());
|
||||
let p = AppsPlugin::new(
|
||||
MockSource::with(vec![("Firefox", "firefox")]),
|
||||
ephemeral_frecency(),
|
||||
);
|
||||
assert!(p.search("").await.is_empty());
|
||||
}
|
||||
|
||||
@@ -322,9 +353,15 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn apps_fuzzy_typo_match() {
|
||||
let p = AppsPlugin::new(MockSource::with(vec![("Firefox", "firefox")]), ephemeral_frecency());
|
||||
let p = AppsPlugin::new(
|
||||
MockSource::with(vec![("Firefox", "firefox")]),
|
||||
ephemeral_frecency(),
|
||||
);
|
||||
let results = p.search("frefox").await;
|
||||
assert!(!results.is_empty(), "nucleo should fuzzy-match 'frefox' to 'Firefox'");
|
||||
assert!(
|
||||
!results.is_empty(),
|
||||
"nucleo should fuzzy-match 'frefox' to 'Firefox'"
|
||||
);
|
||||
assert!(results[0].score.value() > 0);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::path::Path;
|
||||
|
||||
use crate::{AppName, DesktopEntry, DesktopEntrySource, ExecCommand, IconPath};
|
||||
use crate::humanize_category;
|
||||
use crate::{AppName, DesktopEntry, DesktopEntrySource, ExecCommand, IconPath};
|
||||
|
||||
pub struct FsDesktopEntrySource;
|
||||
|
||||
@@ -154,13 +154,15 @@ fn parse_desktop_file(path: &Path) -> Option<DesktopEntry> {
|
||||
"Type" if !is_application => is_application = value.trim() == "Application",
|
||||
"NoDisplay" => no_display = value.trim().eq_ignore_ascii_case("true"),
|
||||
"Categories" if category.is_none() => {
|
||||
category = value.trim()
|
||||
category = value
|
||||
.trim()
|
||||
.split(';')
|
||||
.find(|s| !s.is_empty())
|
||||
.map(|s| humanize_category(s.trim()));
|
||||
}
|
||||
"Keywords" if keywords.is_empty() => {
|
||||
keywords = value.trim()
|
||||
keywords = value
|
||||
.trim()
|
||||
.split(';')
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| s.trim().to_string())
|
||||
@@ -202,7 +204,10 @@ mod exec_tests {
|
||||
|
||||
#[test]
|
||||
fn preserves_quoted_value() {
|
||||
assert_eq!(clean_exec(r#"app --arg="value" %U"#), r#"app --arg="value""#);
|
||||
assert_eq!(
|
||||
clean_exec(r#"app --arg="value" %U"#),
|
||||
r#"app --arg="value""#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -22,8 +22,8 @@ fn strip_numeric_separators(expr: &str) -> String {
|
||||
}
|
||||
|
||||
const MATH_FNS: &[&str] = &[
|
||||
"sqrt", "sin", "cos", "tan", "asin", "acos", "atan",
|
||||
"ln", "log2", "log10", "exp", "abs", "ceil", "floor", "round",
|
||||
"sqrt", "sin", "cos", "tan", "asin", "acos", "atan", "ln", "log2", "log10", "exp", "abs",
|
||||
"ceil", "floor", "round",
|
||||
];
|
||||
|
||||
fn should_eval(query: &str) -> bool {
|
||||
@@ -36,8 +36,8 @@ fn should_eval(query: &str) -> bool {
|
||||
|| MATH_FNS.iter().any(|f| q.starts_with(f))
|
||||
}
|
||||
|
||||
static MATH_CTX: LazyLock<evalexpr::HashMapContext<evalexpr::DefaultNumericTypes>> =
|
||||
LazyLock::new(|| {
|
||||
static MATH_CTX: LazyLock<evalexpr::HashMapContext<evalexpr::DefaultNumericTypes>> = LazyLock::new(
|
||||
|| {
|
||||
use evalexpr::*;
|
||||
context_map! {
|
||||
"pi" => float std::f64::consts::PI,
|
||||
@@ -59,7 +59,8 @@ static MATH_CTX: LazyLock<evalexpr::HashMapContext<evalexpr::DefaultNumericTypes
|
||||
"round" => Function::new(|a: &Value<DefaultNumericTypes>| Ok(Value::from_float(a.as_number()?.round())))
|
||||
}
|
||||
.expect("static math context must be valid")
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
#[async_trait]
|
||||
impl Plugin for CalcPlugin {
|
||||
|
||||
@@ -76,11 +76,7 @@ impl Plugin for FilesPlugin {
|
||||
let full_path = entry.path();
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
let is_dir = full_path.is_dir();
|
||||
let title = if is_dir {
|
||||
format!("{name}/")
|
||||
} else {
|
||||
name
|
||||
};
|
||||
let title = if is_dir { format!("{name}/") } else { name };
|
||||
let path_str = full_path.to_string_lossy().to_string();
|
||||
SearchResult {
|
||||
id: ResultId::new(format!("file-{i}")),
|
||||
|
||||
Reference in New Issue
Block a user