16 Commits
v0.4.7 ... main

Author SHA1 Message Date
c0ea40a393 docs(config): sync example config with current features
- Add dmenu usage examples with | sh pattern
- Fix max_results default (10 → 100)
- Add widget providers (media, weather, pomodoro) with settings
- Add provider badge color customization options
- Add plugin sandbox settings section
- Fix disabled → disabled_plugins, add enabled and registry_url
- Add weather and pomodoro configuration options
2026-01-02 19:00:51 +01:00
44f0915ba9 docs: improve dmenu examples with proper output handling
- Clarify that dmenu outputs to stdout (doesn't execute)
- Add screenshot menu example with | sh pattern
- Use printf instead of echo -e for POSIX compliance
- Add xdg-open example for opening files
- Use shorter -p flag instead of --prompt
2026-01-02 18:56:19 +01:00
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
1aa92ee1e5 chore(owlry-rune): bump version to 0.4.9 2026-01-02 16:18:19 +01:00
9532b3cfde chore(owlry-lua): bump version to 0.4.9 2026-01-02 16:18:18 +01:00
551e5d74ae chore(plugins): bump all plugins to 0.4.9 2026-01-02 16:18:18 +01:00
60eaffb2ab chore: bump version to 0.4.9 2026-01-02 16:18:08 +01:00
6d8d4a9f89 fix(providers): improve app discovery and launch reliability
- Add Keywords field from desktop files to searchable tags
  (fixes apps like Nautilus not found when searching by legacy name)
- Respect XDG_DATA_DIRS with proper fallbacks for app directories
- Add Flatpak, Snap, and Nix application directory support
- Simplify desktop file launch to use gio directly (guaranteed by GTK4)
- Add desktop notifications for launch failures
- Check desktop file existence before launch attempt
2026-01-02 16:18:00 +01:00
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
25 changed files with 456 additions and 173 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.10"
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.10"
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.10"
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.10"
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.10"
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.10"
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.10"
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.10"
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.10"
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.10"
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.10"
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.10"
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.10"
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.10"
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.10"
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.10"
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.10"
dependencies = [ dependencies = [
"chrono", "chrono",
"dirs", "dirs",

View File

@@ -109,29 +109,34 @@ owlry --help # Show all options with examples
### dmenu Mode ### dmenu Mode
Owlry is dmenu-compatible. Pipe input for interactive selection: Owlry is dmenu-compatible. Pipe input for interactive selection - the selected item is printed to stdout (not executed), so you pipe the output to execute it:
```bash ```bash
# Basic selection # Screenshot menu (execute selected command)
echo -e "Option A\nOption B\nOption C" | owlry -m dmenu printf '%s\n' \
"grimblast --notify copy screen" \
# Select from files "grimblast --notify copy area" \
ls ~/Documents | owlry -m dmenu "grimblast --notify edit screen" \
| owlry -m dmenu -p "Screenshot" \
| sh
# Git branch checkout # Git branch checkout
git branch | owlry -m dmenu --prompt "checkout:" | xargs git checkout git branch | owlry -m dmenu -p "checkout" | xargs git checkout
# Kill a process # Kill a process
ps -eo comm | sort -u | owlry -m dmenu --prompt "kill:" | xargs pkill ps -eo comm | sort -u | owlry -m dmenu -p "kill" | xargs pkill
# Select and open a project # Select and open a project
find ~/projects -maxdepth 1 -type d | owlry -m dmenu | xargs code find ~/projects -maxdepth 1 -type d | owlry -m dmenu | xargs code
# Package manager search # Package manager search
pacman -Ssq | owlry -m dmenu --prompt "install:" | xargs sudo pacman -S pacman -Ssq | owlry -m dmenu -p "install" | xargs sudo pacman -S
# Open selected file
ls ~/Documents | owlry -m dmenu | xargs xdg-open
``` ```
The `--prompt` flag sets a custom label for the search input. The `-p` / `--prompt` flag sets a custom label for the search input.
### Keyboard Shortcuts ### Keyboard Shortcuts
@@ -208,8 +213,8 @@ cp /usr/share/doc/owlry/config.example.toml ~/.config/owlry/config.toml
show_icons = true show_icons = true
max_results = 10 max_results = 10
tabs = ["app", "cmd", "uuctl"] tabs = ["app", "cmd", "uuctl"]
# terminal_command = "kitty" # Auto-detected # terminal_command = "kitty" # Auto-detected
# launch_wrapper = "uwsm app --" # Auto-detected # use_uwsm = false # Enable for systemd session integration
[appearance] [appearance]
width = 850 width = 850

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.10"
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.10"
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.10"
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.10"
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.10"
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.10"
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.10"
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.10"
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.10"
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.10"
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.10"
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.10"
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.10"
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.10"
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.10"
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.10"
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.10"
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

@@ -27,11 +27,12 @@ pub struct GeneralConfig {
/// Terminal command (auto-detected if not specified) /// Terminal command (auto-detected if not specified)
#[serde(default)] #[serde(default)]
pub terminal_command: Option<String>, pub terminal_command: Option<String>,
/// Launch wrapper command for app execution. /// Enable uwsm (Universal Wayland Session Manager) for launching apps.
/// Examples: "uwsm app --", "hyprctl dispatch exec --", "systemd-run --user --" /// When enabled, desktop files are launched via `uwsm app -- <file>`
/// If None or empty, launches directly via sh -c /// which starts apps in a proper systemd user session.
/// When disabled (default), apps are launched via `gio launch`.
#[serde(default)] #[serde(default)]
pub launch_wrapper: Option<String>, pub use_uwsm: bool,
/// Provider tabs shown in the header bar. /// Provider tabs shown in the header bar.
/// Valid values: app, cmd, uuctl, bookmark, calc, clip, dmenu, emoji, file, script, ssh, sys, web /// Valid values: app, cmd, uuctl, bookmark, calc, clip, dmenu, emoji, file, script, ssh, sys, web
#[serde(default = "default_tabs")] #[serde(default = "default_tabs")]
@@ -44,7 +45,7 @@ impl Default for GeneralConfig {
show_icons: true, show_icons: true,
max_results: 100, max_results: 100,
terminal_command: None, terminal_command: None,
launch_wrapper: None, use_uwsm: false,
tabs: default_tabs(), tabs: default_tabs(),
} }
} }
@@ -396,28 +397,6 @@ fn default_pomodoro_break() -> u32 {
5 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 /// Detect the best available terminal emulator
/// Fallback chain: /// 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) Ok(config)
} }

View File

@@ -99,23 +99,57 @@ pub fn frecency_file() -> Option<PathBuf> {
// ============================================================================= // =============================================================================
/// System data directories for applications (XDG_DATA_DIRS) /// System data directories for applications (XDG_DATA_DIRS)
///
/// Follows the XDG Base Directory Specification:
/// - $XDG_DATA_HOME/applications (defaults to ~/.local/share/applications)
/// - $XDG_DATA_DIRS/*/applications (defaults to /usr/local/share:/usr/share)
/// - Additional Flatpak and Snap directories
pub fn system_data_dirs() -> Vec<PathBuf> { pub fn system_data_dirs() -> Vec<PathBuf> {
let mut dirs = Vec::new(); let mut dirs = Vec::new();
let mut seen = std::collections::HashSet::new();
// User data directory first // Helper to add unique directories
let mut add_dir = |path: PathBuf| {
if seen.insert(path.clone()) {
dirs.push(path);
}
};
// 1. User data directory first (highest priority)
if let Some(data) = data_home() { if let Some(data) = data_home() {
dirs.push(data.join("applications")); add_dir(data.join("applications"));
} }
// System directories // 2. XDG_DATA_DIRS - parse the environment variable
dirs.push(PathBuf::from("/usr/share/applications")); // Default per spec: /usr/local/share:/usr/share
dirs.push(PathBuf::from("/usr/local/share/applications")); let xdg_data_dirs = std::env::var("XDG_DATA_DIRS")
.unwrap_or_else(|_| "/usr/local/share:/usr/share".to_string());
// Flatpak directories for dir in xdg_data_dirs.split(':') {
if let Some(data) = data_home() { if !dir.is_empty() {
dirs.push(data.join("flatpak/exports/share/applications")); add_dir(PathBuf::from(dir).join("applications"));
}
} }
dirs.push(PathBuf::from("/var/lib/flatpak/exports/share/applications"));
// 3. Always include standard system directories as fallback
// Some environments set XDG_DATA_DIRS without including these
add_dir(PathBuf::from("/usr/share/applications"));
add_dir(PathBuf::from("/usr/local/share/applications"));
// 4. Flatpak directories (user and system)
if let Some(data) = data_home() {
add_dir(data.join("flatpak/exports/share/applications"));
}
add_dir(PathBuf::from("/var/lib/flatpak/exports/share/applications"));
// 5. Snap directories
add_dir(PathBuf::from("/var/lib/snapd/desktop/applications"));
// 6. Nix directories (common on NixOS)
if let Some(home) = dirs::home_dir() {
add_dir(home.join(".nix-profile/share/applications"));
}
add_dir(PathBuf::from("/run/current-system/sw/share/applications"));
dirs dirs
} }

View File

@@ -98,6 +98,15 @@ impl Provider for ApplicationProvider {
// Empty locale list for default locale // Empty locale list for default locale
let locales: &[&str] = &[]; let locales: &[&str] = &[];
// Get current desktop environment(s) for OnlyShowIn/NotShowIn filtering
// XDG_CURRENT_DESKTOP can be colon-separated (e.g., "ubuntu:GNOME")
let current_desktops: Vec<String> = std::env::var("XDG_CURRENT_DESKTOP")
.unwrap_or_default()
.split(':')
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
.collect();
for path in Iter::new(dirs.into_iter()) { for path in Iter::new(dirs.into_iter()) {
let content = match std::fs::read_to_string(&path) { let content = match std::fs::read_to_string(&path) {
Ok(c) => c, Ok(c) => c,
@@ -125,6 +134,24 @@ impl Provider for ApplicationProvider {
continue; continue;
} }
// Apply OnlyShowIn/NotShowIn filters only if we know the current desktop
// If XDG_CURRENT_DESKTOP is not set, show all apps (don't filter)
if !current_desktops.is_empty() {
// OnlyShowIn: if set, current desktop must be in the list
if desktop_entry.only_show_in().is_some_and(|only| {
!current_desktops.iter().any(|de| only.contains(&de.as_str()))
}) {
continue;
}
// NotShowIn: if current desktop is in the list, skip
if desktop_entry.not_show_in().is_some_and(|not| {
current_desktops.iter().any(|de| not.contains(&de.as_str()))
}) {
continue;
}
}
let name = match desktop_entry.name(locales) { let name = match desktop_entry.name(locales) {
Some(n) => n.to_string(), Some(n) => n.to_string(),
None => continue, None => continue,
@@ -135,12 +162,17 @@ impl Provider for ApplicationProvider {
None => continue, None => continue,
}; };
// Extract categories as tags (lowercase for consistency) // Extract categories and keywords as tags (lowercase for consistency)
let tags: Vec<String> = desktop_entry let mut tags: Vec<String> = desktop_entry
.categories() .categories()
.map(|cats| cats.into_iter().map(|s| s.to_lowercase()).collect()) .map(|cats| cats.into_iter().map(|s| s.to_lowercase()).collect())
.unwrap_or_default(); .unwrap_or_default();
// Add keywords for searchability (e.g., Nautilus has Name=Files but Keywords contains "nautilus")
if let Some(keywords) = desktop_entry.keywords(locales) {
tags.extend(keywords.into_iter().map(|s| s.to_lowercase()));
}
let item = LaunchItem { let item = LaunchItem {
id: path.to_string_lossy().to_string(), id: path.to_string_lossy().to_string(),
name, name,
@@ -157,6 +189,13 @@ impl Provider for ApplicationProvider {
debug!("Found {} applications", self.items.len()); debug!("Found {} applications", self.items.len());
#[cfg(feature = "dev-logging")]
debug!(
"XDG_CURRENT_DESKTOP={:?}, scanned dirs count={}",
current_desktops,
Self::get_application_dirs().len()
);
// Sort alphabetically by name // Sort alphabetically by name
self.items.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase())); self.items.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
} }
@@ -210,4 +249,18 @@ mod tests {
"bash -c 'echo %u'" "bash -c 'echo %u'"
); );
} }
#[test]
fn test_clean_desktop_exec_preserves_env() {
// env VAR=value pattern should be preserved
assert_eq!(
clean_desktop_exec_field("env GDK_BACKEND=x11 UBUNTU_MENUPROXY=0 audacity %F"),
"env GDK_BACKEND=x11 UBUNTU_MENUPROXY=0 audacity"
);
// Multiple env vars
assert_eq!(
clean_desktop_exec_field("env FOO=bar BAZ=qux myapp %u"),
"env FOO=bar BAZ=qux myapp"
);
}
} }

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,10 @@ 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>>>,
/// Whether we're in dmenu mode (stdin pipe input)
is_dmenu_mode: bool,
} }
impl MainWindow { impl MainWindow {
@@ -193,6 +199,9 @@ impl MainWindow {
let lazy_state = Rc::new(RefCell::new(LazyLoadState::default())); 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 { let main_window = Self {
window, window,
search_entry, search_entry,
@@ -210,6 +219,8 @@ impl MainWindow {
tab_order, tab_order,
custom_prompt, custom_prompt,
lazy_state, lazy_state,
debounce_source: Rc::new(RefCell::new(None)),
is_dmenu_mode,
}; };
main_window.setup_signals(); main_window.setup_signals();
@@ -554,7 +565,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 +576,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 +619,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 +655,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)
@@ -703,12 +741,14 @@ impl MainWindow {
let mode_label_for_activate = self.mode_label.clone(); let mode_label_for_activate = self.mode_label.clone();
let hints_label_for_activate = self.hints_label.clone(); let hints_label_for_activate = self.hints_label.clone();
let search_entry_for_activate = self.search_entry.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| { self.search_entry.connect_activate(move |entry| {
let selected = results_list_for_activate let selected = results_list_for_activate
.selected_row() .selected_row()
.or_else(|| results_list_for_activate.row_at_index(0)); .or_else(|| results_list_for_activate.row_at_index(0));
// Handle the case where we have a selected item
if let Some(row) = selected { if let Some(row) = selected {
let index = row.index() as usize; let index = row.index() as usize;
let results = current_results_for_activate.borrow(); let results = current_results_for_activate.borrow();
@@ -755,6 +795,10 @@ impl MainWindow {
&providers_for_activate, &providers_for_activate,
); );
if should_close { if should_close {
// In dmenu mode, exit with success code
if is_dmenu_mode_for_activate {
std::process::exit(0);
}
window_for_activate.close(); window_for_activate.close();
} else { } else {
// Trigger search refresh for updated widget state // Trigger search refresh for updated widget state
@@ -762,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);
} }
} }
}); });
@@ -802,6 +856,7 @@ impl MainWindow {
let hints_label = self.hints_label.clone(); let hints_label = self.hints_label.clone();
let submenu_state = self.submenu_state.clone(); let submenu_state = self.submenu_state.clone();
let tab_order = self.tab_order.clone(); let tab_order = self.tab_order.clone();
let is_dmenu_mode = self.is_dmenu_mode;
key_controller.connect_key_pressed(move |_, key, _, modifiers| { key_controller.connect_key_pressed(move |_, key, _, modifiers| {
let ctrl = modifiers.contains(gtk4::gdk::ModifierType::CONTROL_MASK); let ctrl = modifiers.contains(gtk4::gdk::ModifierType::CONTROL_MASK);
@@ -824,6 +879,10 @@ impl MainWindow {
); );
gtk4::glib::Propagation::Stop gtk4::glib::Propagation::Stop
} else { } else {
// In dmenu mode, exit with cancel code (1)
if is_dmenu_mode {
std::process::exit(1);
}
window.close(); window.close();
gtk4::glib::Propagation::Stop gtk4::glib::Propagation::Stop
} }
@@ -841,6 +900,10 @@ impl MainWindow {
); );
gtk4::glib::Propagation::Stop gtk4::glib::Propagation::Stop
} else { } else {
// In dmenu mode, exit with cancel code (1)
if is_dmenu_mode {
std::process::exit(1);
}
window.close(); window.close();
gtk4::glib::Propagation::Stop gtk4::glib::Propagation::Stop
} }
@@ -1238,6 +1301,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);
@@ -1248,18 +1317,89 @@ impl MainWindow {
info!("Launching: {} ({})", item.name, item.command); info!("Launching: {} ({})", item.name, item.command);
#[cfg(feature = "dev-logging")] #[cfg(feature = "dev-logging")]
debug!("[UI] Launch details: terminal={}, provider={:?}", item.terminal, item.provider); debug!("[UI] Launch details: terminal={}, provider={:?}, id={}", item.terminal, item.provider, item.id);
let cmd = if item.terminal { // Check if this is a desktop application (has .desktop file as ID)
let terminal = config.general.terminal_command.as_deref().unwrap_or("xterm"); let is_desktop_app = matches!(item.provider, ProviderType::Application)
format!("{} -e {}", terminal, item.command) && item.id.ends_with(".desktop");
// Desktop files should be launched via proper launchers that implement the
// freedesktop Desktop Entry spec (D-Bus activation, field codes, env vars, etc.)
// We delegate to: uwsm (if configured), gio launch, or gtk-launch as fallback.
//
// Non-desktop items (commands, plugins) use sh -c for shell execution.
let result = if is_desktop_app {
Self::launch_desktop_file(&item.id, config)
} else { } else {
item.command.clone() Self::launch_command(&item.command, item.terminal, config)
}; };
// Detect if this is a shell command vs an application launch if let Err(e) = result {
// Shell commands: playerctl, dbus-send, systemctl, journalctl, or anything with shell operators let msg = format!("Failed to launch '{}': {}", item.name, e);
let is_shell_command = cmd.starts_with("playerctl ") log::error!("{}", msg);
crate::notify::notify("Launch failed", &msg);
}
}
/// Launch a .desktop file.
///
/// When `use_uwsm` is enabled in config, launches via `uwsm app -- <file>`
/// which starts the app in a proper systemd user session.
///
/// 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;
// Check if desktop file exists
if !Path::new(desktop_path).exists() {
let msg = format!("Desktop file not found: {}", desktop_path);
log::error!("{}", msg);
crate::notify::notify("Launch failed", &msg);
return Err(std::io::Error::new(std::io::ErrorKind::NotFound, msg));
}
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()
}
}
/// Launch a shell command (for non-desktop items like PATH commands, plugins, etc.)
fn launch_command(command: &str, terminal: bool, config: &Config) -> std::io::Result<std::process::Child> {
let cmd = if terminal {
let terminal_cmd = config.general.terminal_command.as_deref().unwrap_or("xterm");
format!("{} -e {}", terminal_cmd, command)
} else {
command.to_string()
};
// 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("dbus-send ")
|| cmd.starts_with("systemctl ") || cmd.starts_with("systemctl ")
|| cmd.starts_with("journalctl ") || cmd.starts_with("journalctl ")
@@ -1269,28 +1409,14 @@ impl MainWindow {
|| cmd.contains(" > ") || cmd.contains(" > ")
|| cmd.contains(" < "); || cmd.contains(" < ");
// Use launch wrapper if configured (uwsm, hyprctl, etc.) // Use uwsm for regular commands if enabled (and not a system command)
// But skip wrapper for shell commands - they need sh -c if config.general.use_uwsm && !is_system_command {
let result = match &config.general.launch_wrapper { info!("Launching command via uwsm: {}", cmd);
Some(wrapper) if !wrapper.is_empty() && !is_shell_command => { Command::new("uwsm")
info!("Using launch wrapper: {}", wrapper); .args(["app", "--", "sh", "-c", &cmd])
// Split wrapper into command and args (e.g., "uwsm app --" -> ["uwsm", "app", "--"]) .spawn()
let mut wrapper_parts: Vec<&str> = wrapper.split_whitespace().collect(); } else {
if wrapper_parts.is_empty() { Command::new("sh").arg("-c").arg(&cmd).spawn()
Command::new("sh").arg("-c").arg(&cmd).spawn()
} else {
let wrapper_cmd = wrapper_parts.remove(0);
Command::new(wrapper_cmd)
.args(&wrapper_parts)
.arg(&cmd)
.spawn()
}
}
_ => Command::new("sh").arg("-c").arg(&cmd).spawn(),
};
if let Err(e) = result {
log::error!("Failed to launch '{}': {}", item.name, e);
} }
} }
} }

View File

@@ -17,22 +17,47 @@
# │ Runtimes: /usr/lib/owlry/runtimes/*.so Lua/Rune runtimes │ # │ Runtimes: /usr/lib/owlry/runtimes/*.so Lua/Rune runtimes │
# └─────────────────────────────────────────────────────────────────────┘ # └─────────────────────────────────────────────────────────────────────┘
# ═══════════════════════════════════════════════════════════════════════
# DMENU MODE
# ═══════════════════════════════════════════════════════════════════════
#
# Dmenu mode provides interactive selection from piped input.
# The selected item is printed to stdout (not executed), so pipe
# the output to execute it:
#
# ┌─────────────────────────────────────────────────────────────────────┐
# │ # Screenshot menu │
# │ printf '%s\n' \ │
# │ "grimblast --notify copy screen" \ │
# │ "grimblast --notify copy area" \ │
# │ | owlry -m dmenu -p "Screenshot" \ │
# │ | sh │
# │ │
# │ # Git branch checkout │
# │ git branch | owlry -m dmenu -p "checkout" | xargs git checkout │
# │ │
# │ # Package search │
# │ pacman -Ssq | owlry -m dmenu -p "install" | xargs sudo pacman -S │
# └─────────────────────────────────────────────────────────────────────┘
# ═══════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════
# GENERAL # GENERAL
# ═══════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════
[general] [general]
show_icons = true show_icons = true
max_results = 10 max_results = 100
# Terminal emulator for SSH, scripts, etc. # Terminal emulator for SSH, scripts, etc.
# Auto-detection order: $TERMINAL → xdg-terminal-exec → DE-native → Wayland → X11 → xterm # Auto-detection order: $TERMINAL → xdg-terminal-exec → DE-native → Wayland → X11 → xterm
# Uncomment to override: # Uncomment to override:
# terminal_command = "kitty" # terminal_command = "kitty"
# Launch wrapper for app execution (auto-detected for uwsm/Hyprland) # Enable uwsm (Universal Wayland Session Manager) for launching apps.
# Examples: "uwsm app --", "hyprctl dispatch exec --", "" # When enabled, apps are launched via "uwsm app --" which starts them
# launch_wrapper = "uwsm app --" # 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.) # 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 # Values: app, cmd, uuctl, bookmark, calc, clip, dmenu, emoji, file, script, ssh, sys, web
@@ -62,22 +87,54 @@ border_radius = 12
# text_secondary = "#565f89" # text_secondary = "#565f89"
# accent = "#7aa2f7" # accent = "#7aa2f7"
# accent_bright = "#89b4fa" # accent_bright = "#89b4fa"
#
# Provider badge colors (optional)
# badge_app = "#7aa2f7"
# badge_cmd = "#9ece6a"
# badge_bookmark = "#e0af68"
# badge_calc = "#bb9af7"
# badge_clip = "#7dcfff"
# badge_dmenu = "#c0caf5"
# badge_emoji = "#f7768e"
# badge_file = "#73daca"
# badge_script = "#ff9e64"
# badge_ssh = "#2ac3de"
# badge_sys = "#f7768e"
# badge_uuctl = "#9ece6a"
# badge_web = "#7aa2f7"
# badge_media = "#bb9af7"
# badge_weather = "#7dcfff"
# badge_pomo = "#f7768e"
# ═══════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════
# PLUGINS # PLUGINS
# ═══════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════
# #
# All installed plugins are loaded by default. Use 'disabled' to blacklist. # All installed plugins are loaded by default. Use 'disabled_plugins' to blacklist.
# Plugin IDs: calculator, system, ssh, clipboard, emoji, scripts, bookmarks, # Plugin IDs: calculator, system, ssh, clipboard, emoji, scripts, bookmarks,
# websearch, filesearch, systemd, weather, media, pomodoro # websearch, filesearch, systemd, weather, media, pomodoro
[plugins] [plugins]
enabled = true # Master switch for all plugins
# Plugins to disable (by ID) # Plugins to disable (by ID)
disabled = [] disabled_plugins = []
# Examples: # Examples:
# disabled = ["emoji", "pomodoro"] # Disable specific plugins # disabled_plugins = ["emoji", "pomodoro"] # Disable specific plugins
# disabled = ["weather", "media"] # Disable widget plugins # disabled_plugins = ["weather", "media"] # Disable widget plugins
# Custom plugin registry URL (defaults to official registry)
# registry_url = "https://my-registry.example.com/plugins.json"
# ─────────────────────────────────────────────────────────────────────────
# Sandbox settings (for Lua/Rune script plugins)
# ─────────────────────────────────────────────────────────────────────────
# [plugins.sandbox]
# allow_filesystem = false # Allow file system access beyond plugin dir
# allow_network = false # Allow network requests
# allow_commands = false # Allow shell command execution
# memory_limit = 67108864 # Memory limit in bytes (64 MB default)
# ═══════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════
# PROVIDERS # PROVIDERS
@@ -112,10 +169,26 @@ calculator = true # Calculator (= expression)
websearch = true # Web search (? query) websearch = true # Web search (? query)
# ───────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────
# Plugin settings # Widget providers (displayed at top of results)
# ─────────────────────────────────────────────────────────────────────────
media = true # MPRIS media player controls
weather = false # Weather widget (disabled by default)
pomodoro = false # Pomodoro timer (disabled by default)
# ─────────────────────────────────────────────────────────────────────────
# Provider settings
# ───────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────
# Web search engine # Web search engine
# Options: google, duckduckgo, bing, startpage, searxng, brave, ecosia # Options: google, duckduckgo, bing, startpage, searxng, brave, ecosia
# Or custom URL: "https://search.example.com/?q={query}" # Or custom URL: "https://search.example.com/?q={query}"
search_engine = "duckduckgo" search_engine = "duckduckgo"
# Weather settings (when weather = true)
# weather_provider = "wttr.in" # Options: wttr.in, openweathermap, open-meteo
# weather_location = "Berlin" # City name or coordinates
# weather_api_key = "" # Required for openweathermap
# Pomodoro settings (when pomodoro = true)
# pomodoro_work_mins = 25 # Work session duration
# pomodoro_break_mins = 5 # Break duration