Compare commits

...

3 Commits

Author SHA1 Message Date
3ef9398655 chore: bump all crates to 0.4.8 2026-01-01 23:30:45 +01:00
46bb4bfb38 chore: bump version to 0.4.8 2026-01-01 23:28:09 +01:00
c8aed5faf5 fix(dmenu): print selection to stdout instead of executing
dmenu mode was incorrectly trying to execute the selected item
as a command (via hyprctl/sh). Now it properly prints the
selection to stdout, enabling standard dmenu piping workflows
like: git branch | owlry -m dmenu | xargs git checkout
2026-01-01 23:28:03 +01:00
20 changed files with 135 additions and 79 deletions

34
Cargo.lock generated
View File

@@ -2373,7 +2373,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry" name = "owlry"
version = "0.4.7" version = "0.4.8"
dependencies = [ dependencies = [
"chrono", "chrono",
"clap", "clap",
@@ -2402,7 +2402,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-lua" name = "owlry-lua"
version = "0.4.7" version = "0.4.8"
dependencies = [ dependencies = [
"abi_stable", "abi_stable",
"chrono", "chrono",
@@ -2420,7 +2420,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-plugin-api" name = "owlry-plugin-api"
version = "0.4.7" version = "0.4.8"
dependencies = [ dependencies = [
"abi_stable", "abi_stable",
"serde", "serde",
@@ -2428,7 +2428,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-plugin-bookmarks" name = "owlry-plugin-bookmarks"
version = "0.4.7" version = "0.4.8"
dependencies = [ dependencies = [
"abi_stable", "abi_stable",
"dirs", "dirs",
@@ -2440,7 +2440,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-plugin-calculator" name = "owlry-plugin-calculator"
version = "0.4.7" version = "0.4.8"
dependencies = [ dependencies = [
"abi_stable", "abi_stable",
"meval", "meval",
@@ -2449,7 +2449,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-plugin-clipboard" name = "owlry-plugin-clipboard"
version = "0.4.7" version = "0.4.8"
dependencies = [ dependencies = [
"abi_stable", "abi_stable",
"owlry-plugin-api", "owlry-plugin-api",
@@ -2457,7 +2457,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-plugin-emoji" name = "owlry-plugin-emoji"
version = "0.4.7" version = "0.4.8"
dependencies = [ dependencies = [
"abi_stable", "abi_stable",
"owlry-plugin-api", "owlry-plugin-api",
@@ -2465,7 +2465,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-plugin-filesearch" name = "owlry-plugin-filesearch"
version = "0.4.7" version = "0.4.8"
dependencies = [ dependencies = [
"abi_stable", "abi_stable",
"dirs", "dirs",
@@ -2474,7 +2474,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-plugin-media" name = "owlry-plugin-media"
version = "0.4.7" version = "0.4.8"
dependencies = [ dependencies = [
"abi_stable", "abi_stable",
"owlry-plugin-api", "owlry-plugin-api",
@@ -2482,7 +2482,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-plugin-pomodoro" name = "owlry-plugin-pomodoro"
version = "0.4.7" version = "0.4.8"
dependencies = [ dependencies = [
"abi_stable", "abi_stable",
"dirs", "dirs",
@@ -2494,7 +2494,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-plugin-scripts" name = "owlry-plugin-scripts"
version = "0.4.7" version = "0.4.8"
dependencies = [ dependencies = [
"abi_stable", "abi_stable",
"dirs", "dirs",
@@ -2503,7 +2503,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-plugin-ssh" name = "owlry-plugin-ssh"
version = "0.4.7" version = "0.4.8"
dependencies = [ dependencies = [
"abi_stable", "abi_stable",
"dirs", "dirs",
@@ -2512,7 +2512,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-plugin-system" name = "owlry-plugin-system"
version = "0.4.7" version = "0.4.8"
dependencies = [ dependencies = [
"abi_stable", "abi_stable",
"owlry-plugin-api", "owlry-plugin-api",
@@ -2520,7 +2520,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-plugin-systemd" name = "owlry-plugin-systemd"
version = "0.4.7" version = "0.4.8"
dependencies = [ dependencies = [
"abi_stable", "abi_stable",
"owlry-plugin-api", "owlry-plugin-api",
@@ -2528,7 +2528,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-plugin-weather" name = "owlry-plugin-weather"
version = "0.4.7" version = "0.4.8"
dependencies = [ dependencies = [
"abi_stable", "abi_stable",
"dirs", "dirs",
@@ -2541,7 +2541,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-plugin-websearch" name = "owlry-plugin-websearch"
version = "0.4.7" version = "0.4.8"
dependencies = [ dependencies = [
"abi_stable", "abi_stable",
"owlry-plugin-api", "owlry-plugin-api",
@@ -2549,7 +2549,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-rune" name = "owlry-rune"
version = "0.4.7" version = "0.4.8"
dependencies = [ dependencies = [
"chrono", "chrono",
"dirs", "dirs",

View File

@@ -75,6 +75,24 @@ The script runtimes make this viable without recompiling.
## Technical Debt ## Technical Debt
### Split monorepo for user build efficiency
Currently, a small core fix requires all 16 AUR packages to rebuild (same source tarball). Split into 3 repos:
| Repo | Contents | Versioning |
|------|----------|------------|
| `owlry` | Core binary | Independent |
| `owlry-plugin-api` | ABI interface (crates.io) | Semver, conservative |
| `owlry-plugins` | 13 plugins + 2 runtimes | Independent per plugin |
**Execution order:**
1. Publish `owlry-plugin-api` to crates.io
2. Update monorepo to use crates.io dependency
3. Create `owlry-plugins` repo, move plugins + runtimes
4. Slim current repo to core-only
5. Update AUR PKGBUILDs with new source URLs
**Benefit:** Core bugfix = 1 rebuild. Plugin fix = 1 rebuild. Third-party plugins possible via crates.io.
### Replace meval with evalexpr ### Replace meval with evalexpr
`meval` depends on `nom v1.2.4` which will be rejected by future Rust versions. Migrate calculator plugin and Lua runtime to `evalexpr` v13+. `meval` depends on `nom v1.2.4` which will be rejected by future Rust versions. Migrate calculator plugin and Lua runtime to `evalexpr` v13+.

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-lua" name = "owlry-lua"
version = "0.4.7" version = "0.4.8"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
license.workspace = true license.workspace = true

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-plugin-api" name = "owlry-plugin-api"
version = "0.4.7" version = "0.4.8"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
license.workspace = true license.workspace = true

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-plugin-bookmarks" name = "owlry-plugin-bookmarks"
version = "0.4.7" version = "0.4.8"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
license.workspace = true license.workspace = true

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-plugin-calculator" name = "owlry-plugin-calculator"
version = "0.4.7" version = "0.4.8"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
license.workspace = true license.workspace = true

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-plugin-clipboard" name = "owlry-plugin-clipboard"
version = "0.4.7" version = "0.4.8"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
license.workspace = true license.workspace = true

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-plugin-emoji" name = "owlry-plugin-emoji"
version = "0.4.7" version = "0.4.8"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
license.workspace = true license.workspace = true

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-plugin-filesearch" name = "owlry-plugin-filesearch"
version = "0.4.7" version = "0.4.8"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
license.workspace = true license.workspace = true

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-plugin-media" name = "owlry-plugin-media"
version = "0.4.7" version = "0.4.8"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
license.workspace = true license.workspace = true

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-plugin-pomodoro" name = "owlry-plugin-pomodoro"
version = "0.4.7" version = "0.4.8"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
license.workspace = true license.workspace = true

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-plugin-scripts" name = "owlry-plugin-scripts"
version = "0.4.7" version = "0.4.8"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
license.workspace = true license.workspace = true

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-plugin-ssh" name = "owlry-plugin-ssh"
version = "0.4.7" version = "0.4.8"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
license.workspace = true license.workspace = true

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-plugin-system" name = "owlry-plugin-system"
version = "0.4.7" version = "0.4.8"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
license.workspace = true license.workspace = true

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-plugin-systemd" name = "owlry-plugin-systemd"
version = "0.4.7" version = "0.4.8"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
license.workspace = true license.workspace = true

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-plugin-weather" name = "owlry-plugin-weather"
version = "0.4.7" version = "0.4.8"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
license.workspace = true license.workspace = true

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-plugin-websearch" name = "owlry-plugin-websearch"
version = "0.4.7" version = "0.4.8"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
license.workspace = true license.workspace = true

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-rune" name = "owlry-rune"
version = "0.4.7" version = "0.4.8"
edition = "2024" edition = "2024"
rust-version = "1.90" rust-version = "1.90"
description = "Rune scripting runtime for owlry plugins" description = "Rune scripting runtime for owlry plugins"

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry" name = "owlry"
version = "0.4.7" version = "0.4.8"
edition = "2024" edition = "2024"
rust-version = "1.90" rust-version = "1.90"
description = "A lightweight, owl-themed application launcher for Wayland" description = "A lightweight, owl-themed application launcher for Wayland"

View File

@@ -47,6 +47,8 @@ struct LazyLoadState {
/// Number of items to display initially and per batch /// Number of items to display initially and per batch
const INITIAL_RESULTS: usize = 15; const INITIAL_RESULTS: usize = 15;
const LOAD_MORE_BATCH: usize = 10; const LOAD_MORE_BATCH: usize = 10;
/// Debounce delay for search input (milliseconds)
const SEARCH_DEBOUNCE_MS: u64 = 50;
pub struct MainWindow { pub struct MainWindow {
window: ApplicationWindow, window: ApplicationWindow,
@@ -69,6 +71,8 @@ pub struct MainWindow {
custom_prompt: Option<String>, custom_prompt: Option<String>,
/// Lazy loading state /// Lazy loading state
lazy_state: Rc<RefCell<LazyLoadState>>, lazy_state: Rc<RefCell<LazyLoadState>>,
/// Debounce source ID for cancelling pending searches
debounce_source: Rc<RefCell<Option<gtk4::glib::SourceId>>>,
} }
impl MainWindow { impl MainWindow {
@@ -210,6 +214,7 @@ impl MainWindow {
tab_order, tab_order,
custom_prompt, custom_prompt,
lazy_state, lazy_state,
debounce_source: Rc::new(RefCell::new(None)),
}; };
main_window.setup_signals(); main_window.setup_signals();
@@ -554,7 +559,7 @@ impl MainWindow {
} }
fn setup_signals(&self) { fn setup_signals(&self) {
// Search input handling with prefix detection // Search input handling with prefix detection and debouncing
let providers = self.providers.clone(); let providers = self.providers.clone();
let results_list = self.results_list.clone(); let results_list = self.results_list.clone();
let config = self.config.clone(); let config = self.config.clone();
@@ -565,11 +570,12 @@ impl MainWindow {
let search_entry_for_change = self.search_entry.clone(); let search_entry_for_change = self.search_entry.clone();
let submenu_state = self.submenu_state.clone(); let submenu_state = self.submenu_state.clone();
let lazy_state = self.lazy_state.clone(); let lazy_state = self.lazy_state.clone();
let debounce_source = self.debounce_source.clone();
self.search_entry.connect_changed(move |entry| { self.search_entry.connect_changed(move |entry| {
let raw_query = entry.text(); let raw_query = entry.text();
// If in submenu, filter the submenu items // If in submenu, filter immediately (no debounce needed for small local lists)
if submenu_state.borrow().active { if submenu_state.borrow().active {
let state = submenu_state.borrow(); let state = submenu_state.borrow();
let query = raw_query.to_lowercase(); let query = raw_query.to_lowercase();
@@ -607,7 +613,7 @@ impl MainWindow {
return; return;
} }
// Normal mode: parse prefix and search // Normal mode: update prefix/UI immediately for responsiveness
let parsed = ProviderFilter::parse_query(&raw_query); let parsed = ProviderFilter::parse_query(&raw_query);
{ {
@@ -643,53 +649,79 @@ impl MainWindow {
.set_placeholder_text(Some(&format!("Search {}...", prefix_name))); .set_placeholder_text(Some(&format!("Search {}...", prefix_name)));
} }
let cfg = config.borrow(); // Cancel any pending debounced search
let max_results = cfg.general.max_results; if let Some(source_id) = debounce_source.borrow_mut().take() {
let frecency_weight = cfg.providers.frecency_weight; source_id.remove();
let use_frecency = cfg.providers.frecency;
drop(cfg);
let results: Vec<LaunchItem> = if use_frecency {
providers
.borrow_mut()
.search_with_frecency(&parsed.query, max_results, &filter.borrow(), &frecency.borrow(), frecency_weight, parsed.tag_filter.as_deref())
.into_iter()
.map(|(item, _)| item)
.collect()
} else {
providers
.borrow()
.search_filtered(&parsed.query, max_results, &filter.borrow())
.into_iter()
.map(|(item, _)| item)
.collect()
};
// Clear existing results
while let Some(child) = results_list.first_child() {
results_list.remove(&child);
} }
// Lazy loading: store all results but only display initial batch // Clone references for the debounced closure
let initial_count = INITIAL_RESULTS.min(results.len()); let providers = providers.clone();
{ let results_list = results_list.clone();
let mut lazy = lazy_state.borrow_mut(); let config = config.clone();
lazy.all_results = results.clone(); let frecency = frecency.clone();
lazy.displayed_count = initial_count; let current_results = current_results.clone();
} let filter = filter.clone();
let lazy_state = lazy_state.clone();
let debounce_source_for_closure = debounce_source.clone();
// Display only initial batch // Schedule debounced search
for item in results.iter().take(initial_count) { let source_id = gtk4::glib::timeout_add_local_once(
let row = ResultRow::new(item); std::time::Duration::from_millis(SEARCH_DEBOUNCE_MS),
results_list.append(&row); move || {
} // Clear the source ID since we're now executing
*debounce_source_for_closure.borrow_mut() = None;
if let Some(first_row) = results_list.row_at_index(0) { let cfg = config.borrow();
results_list.select_row(Some(&first_row)); let max_results = cfg.general.max_results;
} let frecency_weight = cfg.providers.frecency_weight;
let use_frecency = cfg.providers.frecency;
drop(cfg);
// current_results holds only what's displayed (for selection/activation) let results: Vec<LaunchItem> = if use_frecency {
*current_results.borrow_mut() = results.into_iter().take(initial_count).collect(); providers
.borrow_mut()
.search_with_frecency(&parsed.query, max_results, &filter.borrow(), &frecency.borrow(), frecency_weight, parsed.tag_filter.as_deref())
.into_iter()
.map(|(item, _)| item)
.collect()
} else {
providers
.borrow()
.search_filtered(&parsed.query, max_results, &filter.borrow())
.into_iter()
.map(|(item, _)| item)
.collect()
};
// Clear existing results
while let Some(child) = results_list.first_child() {
results_list.remove(&child);
}
// Lazy loading: store all results but only display initial batch
let initial_count = INITIAL_RESULTS.min(results.len());
{
let mut lazy = lazy_state.borrow_mut();
lazy.all_results = results.clone();
lazy.displayed_count = initial_count;
}
// Display only initial batch
for item in results.iter().take(initial_count) {
let row = ResultRow::new(item);
results_list.append(&row);
}
if let Some(first_row) = results_list.row_at_index(0) {
results_list.select_row(Some(&first_row));
}
// current_results holds only what's displayed (for selection/activation)
*current_results.borrow_mut() = results.into_iter().take(initial_count).collect();
},
);
*debounce_source.borrow_mut() = Some(source_id);
}); });
// Entry activate signal (Enter key in search entry) // Entry activate signal (Enter key in search entry)
@@ -1238,6 +1270,12 @@ impl MainWindow {
} }
fn launch_item(item: &LaunchItem, config: &Config, frecency: &Rc<RefCell<FrecencyStore>>) { fn launch_item(item: &LaunchItem, config: &Config, frecency: &Rc<RefCell<FrecencyStore>>) {
// dmenu mode: print selection to stdout instead of executing
if matches!(item.provider, ProviderType::Dmenu) {
println!("{}", item.name);
return;
}
// Record this launch for frecency tracking // Record this launch for frecency tracking
if config.providers.frecency { if config.providers.frecency {
frecency.borrow_mut().record_launch(&item.id); frecency.borrow_mut().record_launch(&item.id);