6 Commits

Author SHA1 Message Date
a55567b422 chore(owlry-rune): bump version to 0.4.10 2026-01-02 16:59:22 +01:00
707caefadf chore(owlry-lua): bump version to 0.4.10 2026-01-02 16:59:22 +01:00
78895d34b5 chore(plugins): bump all plugins to 0.4.10 2026-01-02 16:59:14 +01:00
e6f217f19c chore: bump version to 0.4.10 2026-01-02 16:59:06 +01:00
ff04675417 refactor(config): replace launch_wrapper with use_uwsm boolean
- Replace complex auto-detection with explicit use_uwsm config option
- Remove detect_launch_wrapper() function and hyprctl/uwsm auto-detection
- Use gio launch as default (always available via GTK4's glib2 dependency)
- When use_uwsm=true, launch via uwsm app -- for systemd session integration
- Add error handling for when uwsm is enabled but not installed
- Update documentation in README.md, CLAUDE.md, and config.example.toml
2026-01-02 16:57:40 +01:00
b85f85c4da feat(dmenu): add full dmenu compatibility
- Add free-form text input (output typed text when no item matches)
- Add proper exit codes (0=selection, 1=cancelled)
- Detect dmenu mode via ProviderManager::is_dmenu_mode()

This enables standard dmenu usage patterns like:
  echo -e "yes\nno" | owlry -m dmenu && echo "selected"
2026-01-02 16:36:40 +01:00
22 changed files with 119 additions and 110 deletions

34
Cargo.lock generated
View File

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

View File

@@ -208,8 +208,8 @@ cp /usr/share/doc/owlry/config.example.toml ~/.config/owlry/config.toml
show_icons = true
max_results = 10
tabs = ["app", "cmd", "uuctl"]
# terminal_command = "kitty" # Auto-detected
# launch_wrapper = "uwsm app --" # Auto-detected
# terminal_command = "kitty" # Auto-detected
# use_uwsm = false # Enable for systemd session integration
[appearance]
width = 850

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -27,11 +27,12 @@ pub struct GeneralConfig {
/// Terminal command (auto-detected if not specified)
#[serde(default)]
pub terminal_command: Option<String>,
/// Launch wrapper command for app execution.
/// Examples: "uwsm app --", "hyprctl dispatch exec --", "systemd-run --user --"
/// If None or empty, launches directly via sh -c
/// Enable uwsm (Universal Wayland Session Manager) for launching apps.
/// When enabled, desktop files are launched via `uwsm app -- <file>`
/// which starts apps in a proper systemd user session.
/// When disabled (default), apps are launched via `gio launch`.
#[serde(default)]
pub launch_wrapper: Option<String>,
pub use_uwsm: bool,
/// Provider tabs shown in the header bar.
/// Valid values: app, cmd, uuctl, bookmark, calc, clip, dmenu, emoji, file, script, ssh, sys, web
#[serde(default = "default_tabs")]
@@ -44,7 +45,7 @@ impl Default for GeneralConfig {
show_icons: true,
max_results: 100,
terminal_command: None,
launch_wrapper: None,
use_uwsm: false,
tabs: default_tabs(),
}
}
@@ -396,28 +397,6 @@ fn default_pomodoro_break() -> u32 {
5
}
/// Detect the best launch wrapper for the current session
/// Checks for uwsm (Universal Wayland Session Manager) and hyprland
fn detect_launch_wrapper() -> Option<String> {
// Check if running under uwsm (has UWSM_FINALIZE_VARNAMES or similar uwsm env vars)
if (std::env::var("UWSM_FINALIZE_VARNAMES").is_ok()
|| std::env::var("__UWSM_SELECT_TAG").is_ok())
&& command_exists("uwsm") {
debug!("Detected uwsm session, using 'uwsm app --' wrapper");
return Some("uwsm app --".to_string());
}
// Check if running under Hyprland
if std::env::var("HYPRLAND_INSTANCE_SIGNATURE").is_ok()
&& command_exists("hyprctl") {
debug!("Detected Hyprland session, using 'hyprctl dispatch exec --' wrapper");
return Some("hyprctl dispatch exec --".to_string());
}
// No wrapper needed for other environments
debug!("No launch wrapper detected, using direct execution");
None
}
/// Detect the best available terminal emulator
/// Fallback chain:
@@ -578,11 +557,6 @@ impl Config {
}
}
// Auto-detect launch wrapper if not configured
if config.general.launch_wrapper.is_none() {
config.general.launch_wrapper = detect_launch_wrapper();
}
Ok(config)
}

View File

@@ -73,6 +73,8 @@ pub struct MainWindow {
lazy_state: Rc<RefCell<LazyLoadState>>,
/// Debounce source ID for cancelling pending searches
debounce_source: Rc<RefCell<Option<gtk4::glib::SourceId>>>,
/// Whether we're in dmenu mode (stdin pipe input)
is_dmenu_mode: bool,
}
impl MainWindow {
@@ -197,6 +199,9 @@ impl MainWindow {
let lazy_state = Rc::new(RefCell::new(LazyLoadState::default()));
// Check if we're in dmenu mode (stdin pipe input)
let is_dmenu_mode = providers.borrow().is_dmenu_mode();
let main_window = Self {
window,
search_entry,
@@ -215,6 +220,7 @@ impl MainWindow {
custom_prompt,
lazy_state,
debounce_source: Rc::new(RefCell::new(None)),
is_dmenu_mode,
};
main_window.setup_signals();
@@ -735,12 +741,14 @@ impl MainWindow {
let mode_label_for_activate = self.mode_label.clone();
let hints_label_for_activate = self.hints_label.clone();
let search_entry_for_activate = self.search_entry.clone();
let is_dmenu_mode_for_activate = self.is_dmenu_mode;
self.search_entry.connect_activate(move |entry| {
let selected = results_list_for_activate
.selected_row()
.or_else(|| results_list_for_activate.row_at_index(0));
// Handle the case where we have a selected item
if let Some(row) = selected {
let index = row.index() as usize;
let results = current_results_for_activate.borrow();
@@ -787,6 +795,10 @@ impl MainWindow {
&providers_for_activate,
);
if should_close {
// In dmenu mode, exit with success code
if is_dmenu_mode_for_activate {
std::process::exit(0);
}
window_for_activate.close();
} else {
// Trigger search refresh for updated widget state
@@ -794,6 +806,16 @@ impl MainWindow {
}
}
}
return;
}
}
// No item selected/matched - in dmenu mode, output the typed text
if is_dmenu_mode_for_activate {
let text = entry.text();
if !text.is_empty() {
println!("{}", text);
std::process::exit(0);
}
}
});
@@ -834,6 +856,7 @@ impl MainWindow {
let hints_label = self.hints_label.clone();
let submenu_state = self.submenu_state.clone();
let tab_order = self.tab_order.clone();
let is_dmenu_mode = self.is_dmenu_mode;
key_controller.connect_key_pressed(move |_, key, _, modifiers| {
let ctrl = modifiers.contains(gtk4::gdk::ModifierType::CONTROL_MASK);
@@ -856,6 +879,10 @@ impl MainWindow {
);
gtk4::glib::Propagation::Stop
} else {
// In dmenu mode, exit with cancel code (1)
if is_dmenu_mode {
std::process::exit(1);
}
window.close();
gtk4::glib::Propagation::Stop
}
@@ -873,6 +900,10 @@ impl MainWindow {
);
gtk4::glib::Propagation::Stop
} else {
// In dmenu mode, exit with cancel code (1)
if is_dmenu_mode {
std::process::exit(1);
}
window.close();
gtk4::glib::Propagation::Stop
}
@@ -1310,14 +1341,13 @@ impl MainWindow {
}
}
/// Launch a .desktop file using gio (GLib's desktop entry launcher).
/// Launch a .desktop file.
///
/// gio is always available as it's part of glib2, which is a hard dependency
/// of GTK4. It handles D-Bus activation, field codes, Terminal flag, etc.
/// per the freedesktop Desktop Entry specification.
/// When `use_uwsm` is enabled in config, launches via `uwsm app -- <file>`
/// which starts the app in a proper systemd user session.
///
/// Optionally wraps with a session manager (uwsm, hyprctl) for proper
/// process tracking in Wayland compositors.
/// Otherwise, uses `gio launch` which is always available (part of glib2/GTK4)
/// and handles D-Bus activation, field codes, Terminal flag, etc.
fn launch_desktop_file(desktop_path: &str, config: &Config) -> std::io::Result<std::process::Child> {
use std::path::Path;
@@ -1329,25 +1359,32 @@ impl MainWindow {
return Err(std::io::Error::new(std::io::ErrorKind::NotFound, msg));
}
match &config.general.launch_wrapper {
// With wrapper: wrapper manages the process, gio handles the .desktop file
Some(wrapper) if !wrapper.is_empty() => {
info!("Launching via {} + gio launch: {}", wrapper, desktop_path);
let mut parts: Vec<&str> = wrapper.split_whitespace().collect();
let cmd = parts.remove(0);
Command::new(cmd)
.args(&parts)
.args(["gio", "launch"])
.arg(desktop_path)
.spawn()
}
// No wrapper: use gio directly
_ => {
info!("Launching via gio launch: {}", desktop_path);
Command::new("gio")
.args(["launch", desktop_path])
.spawn()
if config.general.use_uwsm {
// Check if uwsm is available
let uwsm_available = Command::new("which")
.arg("uwsm")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false);
if !uwsm_available {
let msg = "uwsm is enabled in config but not installed";
log::error!("{}", msg);
crate::notify::notify("Launch failed", msg);
return Err(std::io::Error::new(std::io::ErrorKind::NotFound, msg));
}
info!("Launching via uwsm: {}", desktop_path);
Command::new("uwsm")
.args(["app", "--", desktop_path])
.spawn()
} else {
info!("Launching via gio: {}", desktop_path);
Command::new("gio")
.args(["launch", desktop_path])
.spawn()
}
}
@@ -1360,8 +1397,9 @@ impl MainWindow {
command.to_string()
};
// Detect shell commands that shouldn't use a wrapper
let is_shell_command = cmd.starts_with("playerctl ")
// Shell/system commands run directly without uwsm wrapper
// (they're typically short-lived or system utilities)
let is_system_command = cmd.starts_with("playerctl ")
|| cmd.starts_with("dbus-send ")
|| cmd.starts_with("systemctl ")
|| cmd.starts_with("journalctl ")
@@ -1371,19 +1409,14 @@ impl MainWindow {
|| cmd.contains(" > ")
|| cmd.contains(" < ");
match &config.general.launch_wrapper {
Some(wrapper) if !wrapper.is_empty() && !is_shell_command => {
info!("Launching command via {}: {}", wrapper, cmd);
let mut parts: Vec<&str> = wrapper.split_whitespace().collect();
let wrapper_cmd = parts.remove(0);
Command::new(wrapper_cmd)
.args(&parts)
.args(["sh", "-c", &cmd])
.spawn()
}
_ => {
Command::new("sh").arg("-c").arg(&cmd).spawn()
}
// Use uwsm for regular commands if enabled (and not a system command)
if config.general.use_uwsm && !is_system_command {
info!("Launching command via uwsm: {}", cmd);
Command::new("uwsm")
.args(["app", "--", "sh", "-c", &cmd])
.spawn()
} else {
Command::new("sh").arg("-c").arg(&cmd).spawn()
}
}
}

View File

@@ -30,9 +30,11 @@ max_results = 10
# Uncomment to override:
# terminal_command = "kitty"
# Launch wrapper for app execution (auto-detected for uwsm/Hyprland)
# Examples: "uwsm app --", "hyprctl dispatch exec --", ""
# launch_wrapper = "uwsm app --"
# Enable uwsm (Universal Wayland Session Manager) for launching apps.
# When enabled, apps are launched via "uwsm app --" which starts them
# in a proper systemd user session for better process management.
# Requires: uwsm to be installed
# use_uwsm = true
# Header tabs - providers shown as toggle buttons (Ctrl+1, Ctrl+2, etc.)
# Values: app, cmd, uuctl, bookmark, calc, clip, dmenu, emoji, file, script, ssh, sys, web