Initial messy version
This commit is contained in:
parent
2715f58991
commit
9080e512c3
8 changed files with 474 additions and 21 deletions
63
Cargo.lock
generated
63
Cargo.lock
generated
|
@ -2,12 +2,24 @@
|
|||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "application-launcher"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"gtk4",
|
||||
"gtk4-layer-shell",
|
||||
"nucleo-matcher",
|
||||
"regex",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -486,6 +498,16 @@ dependencies = [
|
|||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nucleo-matcher"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf33f538733d1a5a3494b836ba913207f14d9d4a1d3cd67030c5061bdd2cac85"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pango"
|
||||
version = "0.20.12"
|
||||
|
@ -555,6 +577,35 @@ dependencies = [
|
|||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
|
@ -599,6 +650,12 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.10"
|
||||
|
@ -681,6 +738,12 @@ version = "1.0.18"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||
|
||||
[[package]]
|
||||
name = "version-compare"
|
||||
version = "0.2.0"
|
||||
|
|
|
@ -6,3 +6,6 @@ edition = "2024"
|
|||
[dependencies]
|
||||
gtk = { package = "gtk4", version = "0.9.6" }
|
||||
gtk4-layer-shell = "0.5.0"
|
||||
nucleo-matcher = "0.3.1"
|
||||
regex = "1.11.1"
|
||||
shlex = "1.3.0"
|
||||
|
|
144
src/main.rs
144
src/main.rs
|
@ -1,3 +1,11 @@
|
|||
pub mod modules;
|
||||
pub mod widget;
|
||||
|
||||
use std::sync::{
|
||||
Arc, Mutex,
|
||||
atomic::{AtomicU32, Ordering},
|
||||
};
|
||||
|
||||
use gtk::{
|
||||
CssProvider, EventControllerKey, Orientation,
|
||||
gdk::{Display, Key},
|
||||
|
@ -5,8 +13,9 @@ use gtk::{
|
|||
prelude::*,
|
||||
};
|
||||
use gtk4_layer_shell::{Edge, Layer, LayerShell};
|
||||
use nucleo_matcher::{Config, Matcher};
|
||||
|
||||
const ITEMS: [&'static str; 3] = ["Test", "Example", "Placeholder"];
|
||||
use crate::modules::{Module, SearchResult, desktop::DesktopModule};
|
||||
|
||||
fn main() -> glib::ExitCode {
|
||||
let app = gtk::Application::builder()
|
||||
|
@ -31,57 +40,161 @@ fn build_ui(application: >k::Application) {
|
|||
window.set_anchor(Edge::Left, false);
|
||||
|
||||
let container = gtk::Box::builder()
|
||||
.css_classes(vec!["container"])
|
||||
.orientation(Orientation::Vertical)
|
||||
.spacing(6)
|
||||
.height_request(600)
|
||||
.build();
|
||||
window.set_child(Some(&container));
|
||||
|
||||
let input = gtk::Entry::builder()
|
||||
.css_classes(vec!["main-input"])
|
||||
.placeholder_text("Start typing...")
|
||||
.width_request(800)
|
||||
.build();
|
||||
|
||||
container.append(&input);
|
||||
|
||||
let below_input_container = gtk::Box::builder()
|
||||
.orientation(Orientation::Horizontal)
|
||||
.spacing(0)
|
||||
.hexpand(true)
|
||||
.vexpand(true)
|
||||
.build();
|
||||
container.append(&below_input_container);
|
||||
|
||||
let result_container = gtk::Box::builder()
|
||||
.css_classes(vec!["result-container"])
|
||||
.orientation(Orientation::Vertical)
|
||||
.spacing(0)
|
||||
.width_request(400)
|
||||
.overflow(gtk::Overflow::Hidden)
|
||||
.build();
|
||||
below_input_container.append(&result_container);
|
||||
|
||||
let detail_container = gtk::Box::builder()
|
||||
.css_classes(vec!["detail-container"])
|
||||
.orientation(Orientation::Vertical)
|
||||
.spacing(2)
|
||||
.height_request(600)
|
||||
.width_request(800)
|
||||
.build();
|
||||
container.append(&result_container);
|
||||
below_input_container.append(&detail_container);
|
||||
|
||||
let current_index = Arc::new(AtomicU32::new(0));
|
||||
let modules: Arc<Vec<Box<dyn Module>>> = Arc::new(vec![Box::new(DesktopModule::new())]);
|
||||
let matcher = Arc::new(Mutex::new(Matcher::new(Config::DEFAULT.match_paths())));
|
||||
let last_results = Arc::new(Mutex::new({
|
||||
let mut matcher = matcher.lock().unwrap();
|
||||
update_results("", &modules, &mut *matcher)
|
||||
}));
|
||||
|
||||
input.connect_changed(glib::clone! {
|
||||
#[weak]
|
||||
result_container,
|
||||
#[weak]
|
||||
current_index,
|
||||
#[weak]
|
||||
last_results,
|
||||
move |input| {
|
||||
remove_all_children(&result_container);
|
||||
let search: &str = &input.text().to_lowercase();
|
||||
for item in ITEMS {
|
||||
if item.to_lowercase().starts_with(&search) {
|
||||
let label = gtk::Label::new(Some(item));
|
||||
result_container.append(&label);
|
||||
let mut matcher = matcher.lock().unwrap();
|
||||
current_index.store(0, Ordering::Relaxed);
|
||||
let mut last_results = last_results.lock().unwrap();
|
||||
*last_results = update_results(search, &modules, &mut *matcher);
|
||||
render_result_list(&result_container, last_results.iter().take(10), 0);
|
||||
}});
|
||||
|
||||
input.connect_activate(glib::clone! {
|
||||
#[weak]
|
||||
current_index,
|
||||
#[weak]
|
||||
last_results,
|
||||
move|_| {
|
||||
let selected_index = current_index.load(Ordering::Relaxed);
|
||||
let last_results = last_results.lock().unwrap();
|
||||
match last_results.get(selected_index as usize) {
|
||||
Some(res) => res.execute(),
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
println!("{}", input.text());
|
||||
std::process::exit(0);
|
||||
}});
|
||||
|
||||
let event_controller = EventControllerKey::new();
|
||||
event_controller.connect_key_pressed(|_, key, _, _| {
|
||||
{
|
||||
let last_results = last_results.clone();
|
||||
event_controller.connect_key_pressed(move |_, key, _, _| {
|
||||
match key {
|
||||
Key::Escape => {
|
||||
std::process::exit(0);
|
||||
}
|
||||
Key::Down | Key::Tab => {
|
||||
let last_results = last_results.lock().unwrap();
|
||||
let _ = current_index.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |i| {
|
||||
if last_results.len() == 0 {
|
||||
return Some(0);
|
||||
}
|
||||
if i as usize == (last_results.len() - 1).min(9) {
|
||||
Some(i)
|
||||
} else {
|
||||
Some(i + 1)
|
||||
}
|
||||
});
|
||||
let selected_index = current_index.load(Ordering::Relaxed);
|
||||
render_result_list(
|
||||
&result_container,
|
||||
last_results.iter().take(10),
|
||||
selected_index as usize,
|
||||
);
|
||||
return glib::Propagation::Stop;
|
||||
}
|
||||
Key::Up | Key::ISO_Left_Tab => {
|
||||
let _ = current_index.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |i| {
|
||||
if i == 0 { Some(0) } else { Some(i - 1) }
|
||||
});
|
||||
let selected_index = current_index.load(Ordering::Relaxed);
|
||||
let last_results = last_results.lock().unwrap();
|
||||
render_result_list(
|
||||
&result_container,
|
||||
last_results.iter().take(10),
|
||||
selected_index as usize,
|
||||
);
|
||||
return glib::Propagation::Stop;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
glib::Propagation::Proceed
|
||||
});
|
||||
}
|
||||
|
||||
window.add_controller(event_controller);
|
||||
|
||||
window.show();
|
||||
}
|
||||
|
||||
fn update_results(
|
||||
search: &str,
|
||||
modules: &[Box<dyn Module>],
|
||||
matcher: &mut Matcher,
|
||||
) -> Vec<Box<dyn SearchResult>> {
|
||||
let mut results = vec![];
|
||||
{
|
||||
for module in modules.iter() {
|
||||
results.extend(module.results_for_search(search, matcher));
|
||||
}
|
||||
}
|
||||
results.sort_by(|a, b| b.confidence().cmp(&a.confidence()));
|
||||
results
|
||||
}
|
||||
|
||||
fn render_result_list<'a>(
|
||||
container: >k::Box,
|
||||
results: impl Iterator<Item = &'a Box<dyn SearchResult>>,
|
||||
current_index: usize,
|
||||
) {
|
||||
remove_all_children(&container);
|
||||
for (i, result) in results.enumerate() {
|
||||
widget::search_result::create(container, result, i == current_index);
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_all_children(b: >k::Box) {
|
||||
let mut child = b.first_child();
|
||||
while let Some(c) = child {
|
||||
|
@ -99,6 +212,9 @@ fn load_css() {
|
|||
gtk::style_context_add_provider_for_display(
|
||||
&Display::default().expect("Could not connect to a display."),
|
||||
&provider,
|
||||
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
|
||||
// We use a really high priority to be able to overwrite user style sheets, as these are
|
||||
// often used for custom themes. We use the theme colors where possible, but sometimes we
|
||||
// need to color something in a way that the theme might not expect.
|
||||
1000, // gtk::STYLE_PROVIDER_PRIORITY_APPLICATION
|
||||
);
|
||||
}
|
||||
|
|
178
src/modules/desktop.rs
Normal file
178
src/modules/desktop.rs
Normal file
|
@ -0,0 +1,178 @@
|
|||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use gtk::{
|
||||
gio::{
|
||||
AppInfo, DesktopAppInfo, Icon,
|
||||
prelude::{AppInfoExt, DesktopAppInfoExtManual},
|
||||
},
|
||||
prelude::BoxExt,
|
||||
};
|
||||
use nucleo_matcher::{
|
||||
Matcher, Utf32Str,
|
||||
pattern::{AtomKind, CaseMatching, Normalization, Pattern},
|
||||
};
|
||||
use regex::Regex;
|
||||
|
||||
use crate::modules::{Module, SearchResult};
|
||||
|
||||
const SEARCH_THRESHOLD: u32 = 0;
|
||||
|
||||
pub struct DesktopResult {
|
||||
pub confidence: u32,
|
||||
pub app_info: DesktopAppInfo,
|
||||
}
|
||||
|
||||
pub struct DesktopModule {
|
||||
pub desktop_entries: Vec<DesktopAppInfo>,
|
||||
}
|
||||
|
||||
impl DesktopModule {
|
||||
pub fn new() -> Self {
|
||||
let desktop_entries = AppInfo::all()
|
||||
.into_iter()
|
||||
.filter_map(|entry| entry.id())
|
||||
.filter_map(|id| DesktopAppInfo::new(&id))
|
||||
.filter(|entry| {
|
||||
!entry.executable().as_os_str().is_empty()
|
||||
&& !entry.display_name().is_empty()
|
||||
&& entry.should_show()
|
||||
})
|
||||
.collect();
|
||||
|
||||
Self { desktop_entries }
|
||||
}
|
||||
}
|
||||
|
||||
impl Module for DesktopModule {
|
||||
fn results_for_search(
|
||||
&self,
|
||||
search: &str,
|
||||
matcher: &mut Matcher,
|
||||
) -> Vec<Box<dyn SearchResult>> {
|
||||
let mut results: Vec<Box<dyn SearchResult>> = vec![];
|
||||
let pattern = Pattern::new(
|
||||
search,
|
||||
CaseMatching::Smart,
|
||||
Normalization::Smart,
|
||||
AtomKind::Fuzzy,
|
||||
);
|
||||
for app in self.desktop_entries.iter() {
|
||||
let name: String = app.name().into();
|
||||
let mut name_buf = Vec::new();
|
||||
let name = Utf32Str::new(&name, &mut name_buf);
|
||||
let score = pattern.score(name, matcher).unwrap_or(0);
|
||||
if score > SEARCH_THRESHOLD {
|
||||
results.push(Box::new(DesktopResult {
|
||||
confidence: score,
|
||||
app_info: app.clone(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchResult for DesktopResult {
|
||||
fn confidence(&self) -> u32 {
|
||||
self.confidence
|
||||
}
|
||||
fn label(&self) -> String {
|
||||
self.app_info.name().into()
|
||||
}
|
||||
fn description(&self) -> Option<String> {
|
||||
self.app_info.description().map(Into::into)
|
||||
}
|
||||
|
||||
fn icon(&self) -> Option<Icon> {
|
||||
self.app_info.icon()
|
||||
}
|
||||
|
||||
fn render_detail(&self, container: >k::Box) {
|
||||
let text = format!("[{}] {}", self.confidence, self.app_info.name(),);
|
||||
let label = gtk::Label::builder()
|
||||
.label(text)
|
||||
.halign(gtk::Align::Start)
|
||||
.build();
|
||||
container.append(&label);
|
||||
}
|
||||
|
||||
fn execute(&self) {
|
||||
let app_id = self.app_info.id();
|
||||
let mut app_exec = match self
|
||||
.app_info
|
||||
.commandline()
|
||||
.map(|p| p.to_str().map(ToOwned::to_owned))
|
||||
.flatten()
|
||||
{
|
||||
Some(app_exec) => app_exec,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let mut app_dir = None;
|
||||
if let Some(mut desktop_entry_path) = self.app_info.filename() {
|
||||
if let Some(desktop_entry_path) = desktop_entry_path.to_str() {
|
||||
app_exec = app_exec.replace("%k", desktop_entry_path);
|
||||
}
|
||||
desktop_entry_path.pop();
|
||||
app_dir = Some(desktop_entry_path);
|
||||
}
|
||||
|
||||
app_exec = Regex::new(r"\%[uUfFdDnNickvm]")
|
||||
.unwrap()
|
||||
.replace(&app_exec, "")
|
||||
.to_string();
|
||||
|
||||
let working_dir = match self
|
||||
.app_info
|
||||
.string("Path")
|
||||
.map(|path| PathBuf::from_str(&path).ok())
|
||||
.flatten()
|
||||
{
|
||||
Some(path) => Some(path),
|
||||
None => app_dir,
|
||||
};
|
||||
|
||||
if self.app_info.boolean("DBusActivatable") {
|
||||
if let Some(app_id) = app_id {
|
||||
launch_cmd(vec!["gapplication", "launch", &app_id], working_dir);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if self.app_info.boolean("Terminal") {
|
||||
// TODO: Make terminal emulator configurable
|
||||
launch_cmd(vec!["kitty", &app_exec], working_dir);
|
||||
} else {
|
||||
if let Some(cmd) = shlex::split(&app_exec) {
|
||||
launch_cmd(cmd, working_dir);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn launch_cmd(cmd: Vec<impl AsRef<str>>, working_dir: Option<PathBuf>) {
|
||||
let cmd_prefix = vec!["systemd-run", "--user", "--scope"];
|
||||
let mut cmd_prefix = cmd_prefix.iter().map(|s| Path::new(s)).collect::<Vec<_>>();
|
||||
let cmd_suffix = cmd
|
||||
.iter()
|
||||
.map(|s| Path::new(s.as_ref()))
|
||||
.collect::<Vec<_>>();
|
||||
cmd_prefix.extend(cmd_suffix);
|
||||
let cmd = cmd_prefix;
|
||||
let env = gtk::glib::environ();
|
||||
let env = env.iter().map(|f| Path::new(f)).collect::<Vec<_>>();
|
||||
|
||||
gtk::glib::spawn_async(
|
||||
working_dir,
|
||||
&cmd,
|
||||
&env,
|
||||
gtk::glib::SpawnFlags::SEARCH_PATH_FROM_ENVP | gtk::glib::SpawnFlags::SEARCH_PATH,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
19
src/modules/mod.rs
Normal file
19
src/modules/mod.rs
Normal file
|
@ -0,0 +1,19 @@
|
|||
use gtk::gio::Icon;
|
||||
use nucleo_matcher::Matcher;
|
||||
|
||||
pub mod desktop;
|
||||
|
||||
pub trait Module {
|
||||
fn results_for_search(&self, search: &str, matcher: &mut Matcher)
|
||||
-> Vec<Box<dyn SearchResult>>;
|
||||
}
|
||||
|
||||
pub trait SearchResult {
|
||||
fn confidence(&self) -> u32;
|
||||
fn label(&self) -> String;
|
||||
fn description(&self) -> Option<String>;
|
||||
fn icon(&self) -> Option<Icon>;
|
||||
|
||||
fn render_detail(&self, container: >k::Box);
|
||||
fn execute(&self);
|
||||
}
|
|
@ -1,7 +1,44 @@
|
|||
.main-input {
|
||||
background: @theme_bg_color;
|
||||
font-size: 32px;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border: 2px solid @borders;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.main-input text {
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
.result {
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.result.selected {
|
||||
color: @theme_selected_fg_color;
|
||||
background: @theme_selected_bg_color;
|
||||
}
|
||||
|
||||
.result-container {
|
||||
border: 2px solid @borders;
|
||||
/* We already get a border from the search input, so we don't need to repeat
|
||||
* it here. */
|
||||
border-top: none;
|
||||
background: @theme_bg_color;
|
||||
border-bottom-left-radius: 6px;
|
||||
}
|
||||
|
||||
.result-name {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.detail-container {
|
||||
background: gray;
|
||||
border-bottom-right-radius: 6px;
|
||||
}
|
||||
|
||||
window {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
|
|
1
src/widget/mod.rs
Normal file
1
src/widget/mod.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub mod search_result;
|
36
src/widget/search_result.rs
Normal file
36
src/widget/search_result.rs
Normal file
|
@ -0,0 +1,36 @@
|
|||
use gtk::{Orientation, pango::EllipsizeMode, prelude::BoxExt};
|
||||
|
||||
use crate::modules::SearchResult;
|
||||
|
||||
pub fn create(container: >k::Box, result: &Box<dyn SearchResult>, is_selected: bool) {
|
||||
let res_box = gtk::Box::builder()
|
||||
.orientation(Orientation::Horizontal)
|
||||
.css_classes(if is_selected {
|
||||
vec!["result", "selected"]
|
||||
} else {
|
||||
vec!["result"]
|
||||
})
|
||||
.spacing(8)
|
||||
.build();
|
||||
|
||||
// If there is an icon display it, otherwise display an empty box.
|
||||
match result.icon() {
|
||||
Some(icon) => res_box.append(>k::Image::builder().gicon(&icon).pixel_size(16).build()),
|
||||
None => res_box.append(>k::Box::new(Orientation::Horizontal, 0)),
|
||||
}
|
||||
|
||||
let detail_box = gtk::Box::new(Orientation::Vertical, 2);
|
||||
detail_box.append(
|
||||
>k::Label::builder()
|
||||
.label(result.label())
|
||||
.css_classes(vec!["result-name"])
|
||||
.halign(gtk::Align::Start)
|
||||
.max_width_chars(40)
|
||||
.ellipsize(EllipsizeMode::End)
|
||||
.hexpand(true)
|
||||
.build(),
|
||||
);
|
||||
res_box.append(&detail_box);
|
||||
|
||||
container.append(&res_box);
|
||||
}
|
Loading…
Add table
Reference in a new issue