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.
|
# It is not intended for manual editing.
|
||||||
version = 4
|
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]]
|
[[package]]
|
||||||
name = "application-launcher"
|
name = "application-launcher"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gtk4",
|
"gtk4",
|
||||||
"gtk4-layer-shell",
|
"gtk4-layer-shell",
|
||||||
|
"nucleo-matcher",
|
||||||
|
"regex",
|
||||||
|
"shlex",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -486,6 +498,16 @@ dependencies = [
|
||||||
"autocfg",
|
"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]]
|
[[package]]
|
||||||
name = "pango"
|
name = "pango"
|
||||||
version = "0.20.12"
|
version = "0.20.12"
|
||||||
|
@ -555,6 +577,35 @@ dependencies = [
|
||||||
"proc-macro2",
|
"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]]
|
[[package]]
|
||||||
name = "rustc_version"
|
name = "rustc_version"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
|
@ -599,6 +650,12 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shlex"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.10"
|
version = "0.4.10"
|
||||||
|
@ -681,6 +738,12 @@ version = "1.0.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-segmentation"
|
||||||
|
version = "1.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version-compare"
|
name = "version-compare"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
|
|
@ -6,3 +6,6 @@ edition = "2024"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
gtk = { package = "gtk4", version = "0.9.6" }
|
gtk = { package = "gtk4", version = "0.9.6" }
|
||||||
gtk4-layer-shell = "0.5.0"
|
gtk4-layer-shell = "0.5.0"
|
||||||
|
nucleo-matcher = "0.3.1"
|
||||||
|
regex = "1.11.1"
|
||||||
|
shlex = "1.3.0"
|
||||||
|
|
158
src/main.rs
158
src/main.rs
|
@ -1,3 +1,11 @@
|
||||||
|
pub mod modules;
|
||||||
|
pub mod widget;
|
||||||
|
|
||||||
|
use std::sync::{
|
||||||
|
Arc, Mutex,
|
||||||
|
atomic::{AtomicU32, Ordering},
|
||||||
|
};
|
||||||
|
|
||||||
use gtk::{
|
use gtk::{
|
||||||
CssProvider, EventControllerKey, Orientation,
|
CssProvider, EventControllerKey, Orientation,
|
||||||
gdk::{Display, Key},
|
gdk::{Display, Key},
|
||||||
|
@ -5,8 +13,9 @@ use gtk::{
|
||||||
prelude::*,
|
prelude::*,
|
||||||
};
|
};
|
||||||
use gtk4_layer_shell::{Edge, Layer, LayerShell};
|
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 {
|
fn main() -> glib::ExitCode {
|
||||||
let app = gtk::Application::builder()
|
let app = gtk::Application::builder()
|
||||||
|
@ -31,57 +40,161 @@ fn build_ui(application: >k::Application) {
|
||||||
window.set_anchor(Edge::Left, false);
|
window.set_anchor(Edge::Left, false);
|
||||||
|
|
||||||
let container = gtk::Box::builder()
|
let container = gtk::Box::builder()
|
||||||
|
.css_classes(vec!["container"])
|
||||||
.orientation(Orientation::Vertical)
|
.orientation(Orientation::Vertical)
|
||||||
.spacing(6)
|
.height_request(600)
|
||||||
.build();
|
.build();
|
||||||
window.set_child(Some(&container));
|
window.set_child(Some(&container));
|
||||||
|
|
||||||
let input = gtk::Entry::builder()
|
let input = gtk::Entry::builder()
|
||||||
.css_classes(vec!["main-input"])
|
.css_classes(vec!["main-input"])
|
||||||
.placeholder_text("Start typing...")
|
.placeholder_text("Start typing...")
|
||||||
.width_request(800)
|
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
container.append(&input);
|
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()
|
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)
|
.orientation(Orientation::Vertical)
|
||||||
.spacing(2)
|
.spacing(2)
|
||||||
.height_request(600)
|
.width_request(800)
|
||||||
.build();
|
.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! {
|
input.connect_changed(glib::clone! {
|
||||||
#[weak]
|
#[weak]
|
||||||
result_container,
|
result_container,
|
||||||
|
#[weak]
|
||||||
|
current_index,
|
||||||
|
#[weak]
|
||||||
|
last_results,
|
||||||
move |input| {
|
move |input| {
|
||||||
remove_all_children(&result_container);
|
|
||||||
let search: &str = &input.text().to_lowercase();
|
let search: &str = &input.text().to_lowercase();
|
||||||
for item in ITEMS {
|
let mut matcher = matcher.lock().unwrap();
|
||||||
if item.to_lowercase().starts_with(&search) {
|
current_index.store(0, Ordering::Relaxed);
|
||||||
let label = gtk::Label::new(Some(item));
|
let mut last_results = last_results.lock().unwrap();
|
||||||
result_container.append(&label);
|
*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();
|
let event_controller = EventControllerKey::new();
|
||||||
event_controller.connect_key_pressed(|_, key, _, _| {
|
{
|
||||||
match key {
|
let last_results = last_results.clone();
|
||||||
Key::Escape => {
|
event_controller.connect_key_pressed(move |_, key, _, _| {
|
||||||
std::process::exit(0);
|
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
|
||||||
}
|
});
|
||||||
glib::Propagation::Proceed
|
}
|
||||||
});
|
|
||||||
|
|
||||||
window.add_controller(event_controller);
|
window.add_controller(event_controller);
|
||||||
|
|
||||||
window.show();
|
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) {
|
fn remove_all_children(b: >k::Box) {
|
||||||
let mut child = b.first_child();
|
let mut child = b.first_child();
|
||||||
while let Some(c) = child {
|
while let Some(c) = child {
|
||||||
|
@ -99,6 +212,9 @@ fn load_css() {
|
||||||
gtk::style_context_add_provider_for_display(
|
gtk::style_context_add_provider_for_display(
|
||||||
&Display::default().expect("Could not connect to a display."),
|
&Display::default().expect("Could not connect to a display."),
|
||||||
&provider,
|
&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 {
|
.main-input {
|
||||||
|
background: @theme_bg_color;
|
||||||
font-size: 32px;
|
font-size: 32px;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
border: 2px solid @borders;
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-input text {
|
.main-input text {
|
||||||
margin: 8px;
|
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