Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4ff054afe0 | |||
| 8547dfa951 | |||
| e1a6650b7d | |||
| 6ae8c734d2 | |||
| cf48d53c57 | |||
| 41cd212264 | |||
| 7cdb97d743 | |||
| 98ac769b29 | |||
| e73793dd6e | |||
| e680032d0e | |||
| 738fecc6da | |||
| a1351f05e9 | |||
| 7118498773 | |||
| 3d05e560b1 | |||
| 604b902261 | |||
| bb0b0dfa87 | |||
| fc4dde32eb | |||
| cc1ad7bbb7 | |||
| 16ba5b642a | |||
| 1608582cbd |
32
CLAUDE.md
Normal file
32
CLAUDE.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Owlry - Claude Code Instructions
|
||||
|
||||
## Release Workflow
|
||||
|
||||
Always use `just` for releases and AUR deployment:
|
||||
|
||||
```bash
|
||||
# Bump version (updates Cargo.toml + Cargo.lock, commits)
|
||||
just bump 0.x.y
|
||||
|
||||
# Push and create tag
|
||||
git push && just tag
|
||||
|
||||
# Update AUR package
|
||||
just aur-update
|
||||
|
||||
# Review changes, then publish
|
||||
just aur-publish
|
||||
```
|
||||
|
||||
Do NOT manually edit Cargo.toml for version bumps - use `just bump`.
|
||||
|
||||
## Available just recipes
|
||||
|
||||
- `just build` / `just release` - Build debug/release
|
||||
- `just check` - Run cargo check + clippy
|
||||
- `just test` - Run tests
|
||||
- `just bump <version>` - Bump version
|
||||
- `just tag` - Create and push git tag
|
||||
- `just aur-update` - Update PKGBUILD checksums
|
||||
- `just aur-publish` - Commit and push to AUR
|
||||
- `just aur-test` - Test PKGBUILD locally
|
||||
240
Cargo.lock
generated
240
Cargo.lock
generated
@@ -11,6 +11,15 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.21"
|
||||
@@ -90,6 +99,12 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.19.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.11.0"
|
||||
@@ -145,6 +160,20 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.53"
|
||||
@@ -191,6 +220,12 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "5.0.1"
|
||||
@@ -267,6 +302,12 @@ version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff"
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "freedesktop-desktop-entry"
|
||||
version = "0.7.19"
|
||||
@@ -694,6 +735,30 @@ version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.64"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.12.1"
|
||||
@@ -710,6 +775,12 @@ version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||
|
||||
[[package]]
|
||||
name = "jiff"
|
||||
version = "0.2.17"
|
||||
@@ -734,6 +805,16 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "khronos_api"
|
||||
version = "3.1.0"
|
||||
@@ -805,6 +886,16 @@ dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "meval"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f79496a5651c8d57cd033c5add8ca7ee4e3d5f7587a4777484640d9cb60392d9"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.1.1"
|
||||
@@ -816,6 +907,21 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "1.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5b8c256fd9471521bcb84c3cdba98921497f1a331cbc15b8030fc63b82050ce"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc"
|
||||
version = "0.2.7"
|
||||
@@ -845,6 +951,12 @@ dependencies = [
|
||||
"objc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.2"
|
||||
@@ -859,8 +971,9 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "owlry"
|
||||
version = "0.1.3"
|
||||
version = "0.3.3"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"clap",
|
||||
"dirs",
|
||||
"env_logger",
|
||||
@@ -870,7 +983,9 @@ dependencies = [
|
||||
"gtk4-layer-shell",
|
||||
"libc",
|
||||
"log",
|
||||
"meval",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"toml 0.8.23",
|
||||
@@ -1009,6 +1124,12 @@ dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.27"
|
||||
@@ -1045,6 +1166,19 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.148"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.9"
|
||||
@@ -1318,6 +1452,51 @@ version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
@@ -1340,12 +1519,65 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
@@ -1514,3 +1746,9 @@ name = "xml-rs"
|
||||
version = "0.8.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6d6085d62852e35540689d1f97ad663e3971fc19cf5eceab364d62c646ea167"
|
||||
|
||||
11
Cargo.toml
11
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "owlry"
|
||||
version = "0.1.3"
|
||||
version = "0.3.3"
|
||||
edition = "2024"
|
||||
rust-version = "1.90"
|
||||
description = "A lightweight, owl-themed application launcher for Wayland"
|
||||
@@ -46,6 +46,15 @@ toml = "0.8"
|
||||
# CLI argument parsing
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
|
||||
# Math expression evaluation for calculator
|
||||
meval = "0.2"
|
||||
|
||||
# JSON serialization for data persistence
|
||||
serde_json = "1"
|
||||
|
||||
# Date/time for frecency calculations
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
|
||||
222
README.md
222
README.md
@@ -10,12 +10,14 @@ A lightweight, owl-themed application launcher for Wayland, built with GTK4 and
|
||||
|
||||
## Features
|
||||
|
||||
- **Provider-based architecture** - Search applications, PATH commands, and systemd user services
|
||||
- **Provider-based architecture** - Search applications, commands, system actions, SSH hosts, clipboard history, bookmarks, emoji, and more
|
||||
- **Fuzzy search** - Fast, typo-tolerant matching across all providers
|
||||
- **Filter tabs & prefixes** - Scope searches with UI tabs or `:app`, `:cmd`, `:uuctl` prefixes
|
||||
- **Systemd integration** - Manage user services with submenu actions (start/stop/restart/status/journal)
|
||||
- **Filter tabs & prefixes** - Scope searches with UI tabs or `:app`, `:cmd`, `:sys` prefixes
|
||||
- **Calculator** - Quick math with `= 5+3` or `calc sin(pi/2)`
|
||||
- **Web search** - Search the web with `? query` or `web query`
|
||||
- **File search** - Find files with `/ filename` or `find config` (requires `fd` or `locate`)
|
||||
- **Frecency ranking** - Frequently/recently used items rank higher
|
||||
- **GTK4 theming** - Respects system theme by default, with optional custom themes
|
||||
- **CSS variables** - Full customization via config or custom stylesheets
|
||||
- **Wayland native** - Uses Layer Shell for proper overlay behavior
|
||||
|
||||
## Installation
|
||||
@@ -45,6 +47,16 @@ sudo apt install libgtk-4-dev libgtk4-layer-shell-dev
|
||||
sudo dnf install gtk4-devel gtk4-layer-shell-devel
|
||||
```
|
||||
|
||||
#### Optional dependencies
|
||||
|
||||
```bash
|
||||
# For clipboard history
|
||||
sudo pacman -S cliphist wl-clipboard
|
||||
|
||||
# For file search
|
||||
sudo pacman -S fd # or: mlocate
|
||||
```
|
||||
|
||||
#### Build
|
||||
|
||||
Requires Rust 1.90 or later.
|
||||
@@ -79,7 +91,7 @@ owlry --help
|
||||
|-----|--------|
|
||||
| `Enter` | Launch selected item |
|
||||
| `Escape` | Close launcher / exit submenu |
|
||||
| `↑` / `↓` | Navigate results |
|
||||
| `Up` / `Down` | Navigate results |
|
||||
| `Tab` | Cycle filter modes |
|
||||
| `Shift+Tab` | Cycle filter modes (reverse) |
|
||||
| `Ctrl+1` | Toggle Applications filter |
|
||||
@@ -88,24 +100,119 @@ owlry --help
|
||||
|
||||
### Search Prefixes
|
||||
|
||||
| Prefix | Scope |
|
||||
|--------|-------|
|
||||
| `:app ` | Applications only |
|
||||
| `:cmd ` | PATH commands only |
|
||||
| `:uuctl ` | systemd user services only |
|
||||
Filter results by provider using prefixes:
|
||||
|
||||
Example: `:cmd git` searches only PATH commands for "git"
|
||||
| Prefix | Provider | Example |
|
||||
|--------|----------|---------|
|
||||
| `:app` | Applications | `:app firefox` |
|
||||
| `:cmd` | PATH commands | `:cmd git` |
|
||||
| `:sys` | System actions | `:sys shutdown` |
|
||||
| `:ssh` | SSH hosts | `:ssh server` |
|
||||
| `:clip` | Clipboard history | `:clip password` |
|
||||
| `:bm` | Browser bookmarks | `:bm github` |
|
||||
| `:emoji` | Emoji picker | `:emoji heart` |
|
||||
| `:script` | Custom scripts | `:script backup` |
|
||||
| `:file` | File search | `:file config.toml` |
|
||||
| `:calc` | Calculator | `:calc 5+3` |
|
||||
| `:web` | Web search | `:web rust docs` |
|
||||
| `:uuctl` | systemd services | `:uuctl docker` |
|
||||
|
||||
### Trigger Prefixes
|
||||
|
||||
Some providers can be triggered directly without filter mode:
|
||||
|
||||
| Trigger | Provider | Example |
|
||||
|---------|----------|---------|
|
||||
| `=` | Calculator | `= 5+3` or `=5*2` |
|
||||
| `calc ` | Calculator | `calc sqrt(16)` |
|
||||
| `?` | Web search | `? rust programming` |
|
||||
| `web ` | Web search | `web linux tips` |
|
||||
| `search ` | Web search | `search owlry` |
|
||||
| `/` | File search | `/ .bashrc` |
|
||||
| `find ` | File search | `find config` |
|
||||
|
||||
## Providers
|
||||
|
||||
### Applications
|
||||
Searches `.desktop` files from standard XDG directories.
|
||||
|
||||
### Commands
|
||||
Searches executable files in `$PATH`.
|
||||
|
||||
### System
|
||||
Quick access to system actions:
|
||||
- Shutdown, Reboot, Suspend, Hibernate
|
||||
- Lock Screen, Log Out
|
||||
- **Reboot into BIOS** - Restart directly into UEFI/BIOS setup
|
||||
|
||||
### SSH
|
||||
Parses `~/.ssh/config` and offers quick connections to configured hosts. Opens in your configured terminal.
|
||||
|
||||
### Clipboard (requires cliphist)
|
||||
Search and paste from clipboard history. Requires `cliphist` and `wl-clipboard`:
|
||||
```bash
|
||||
sudo pacman -S cliphist wl-clipboard
|
||||
```
|
||||
|
||||
### Bookmarks
|
||||
Reads bookmarks from Chromium-based browsers:
|
||||
- Chrome, Chromium, Brave, Edge, Vivaldi
|
||||
|
||||
### Emoji
|
||||
Search 300+ emojis by name or keywords. Selected emoji is copied to clipboard via `wl-copy`.
|
||||
|
||||
### Scripts
|
||||
Runs executable scripts from `~/.config/owlry/scripts/`. Create the directory and add your scripts:
|
||||
```bash
|
||||
mkdir -p ~/.config/owlry/scripts
|
||||
echo '#!/bin/bash
|
||||
# My backup script
|
||||
rsync -av ~/Documents /backup/' > ~/.config/owlry/scripts/backup
|
||||
chmod +x ~/.config/owlry/scripts/backup
|
||||
```
|
||||
|
||||
### Calculator
|
||||
Evaluate math expressions with `= expr` or `calc expr`:
|
||||
- Basic: `= 5+3`, `= 10/3`
|
||||
- Functions: `= sqrt(16)`, `= sin(pi/2)`
|
||||
- Constants: `= pi`, `= e`
|
||||
|
||||
### Web Search
|
||||
Search the web with `? query` or `web query`. Configurable search engine:
|
||||
- Google, DuckDuckGo, Bing, Brave, Ecosia, Startpage, SearXNG
|
||||
- Or custom URL with `{query}` placeholder
|
||||
|
||||
### File Search (requires fd or locate)
|
||||
Search files with `/ pattern` or `find pattern`:
|
||||
```bash
|
||||
sudo pacman -S fd # recommended, faster
|
||||
# or
|
||||
sudo pacman -S mlocate && sudo updatedb
|
||||
```
|
||||
|
||||
### systemd User Services
|
||||
Lists and controls user-level systemd services. Select a service to access actions:
|
||||
- Start / Stop / Restart / Reload
|
||||
- Kill (force stop)
|
||||
- Status (opens in terminal)
|
||||
- Journal (live logs in terminal)
|
||||
- Enable / Disable (autostart)
|
||||
|
||||
## Configuration
|
||||
|
||||
Configuration file: `~/.config/owlry/config.toml`
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.config/owlry
|
||||
cp /usr/share/doc/owlry/config.example.toml ~/.config/owlry/config.toml
|
||||
```
|
||||
|
||||
```toml
|
||||
[general]
|
||||
show_icons = true
|
||||
max_results = 10
|
||||
# terminal_command = "kitty" # Auto-detected if not set
|
||||
# launch_wrapper = "uwsm app --" # Auto-detected for uwsm/hyprland sessions
|
||||
# launch_wrapper = "uwsm app --" # Auto-detected for uwsm/hyprland
|
||||
|
||||
[appearance]
|
||||
width = 600
|
||||
@@ -114,15 +221,22 @@ font_size = 14
|
||||
border_radius = 12
|
||||
# theme = "owl" # Optional: "owl" or custom theme name
|
||||
|
||||
# [appearance.colors]
|
||||
# Override individual colors (optional)
|
||||
# accent = "#e0af68"
|
||||
# background = "#1a1b26"
|
||||
|
||||
[providers]
|
||||
applications = true
|
||||
commands = true
|
||||
uuctl = true
|
||||
calculator = true
|
||||
websearch = true
|
||||
search_engine = "duckduckgo" # google, bing, brave, ecosia, startpage, searxng
|
||||
system = true
|
||||
ssh = true
|
||||
clipboard = true
|
||||
bookmarks = true
|
||||
emoji = true
|
||||
scripts = true
|
||||
files = true
|
||||
frecency = true
|
||||
frecency_weight = 0.3 # 0.0 = disabled, 1.0 = strong boost
|
||||
```
|
||||
|
||||
### Default Values
|
||||
@@ -131,8 +245,8 @@ uuctl = true
|
||||
|---------|---------|
|
||||
| `show_icons` | `true` |
|
||||
| `max_results` | `10` |
|
||||
| `terminal_command` | Auto-detected ($TERMINAL → xdg-terminal-exec → kitty/alacritty/etc) |
|
||||
| `launch_wrapper` | Auto-detected (uwsm → hyprctl → none) |
|
||||
| `terminal_command` | Auto-detected ($TERMINAL -> xdg-terminal-exec -> kitty/alacritty/etc) |
|
||||
| `launch_wrapper` | Auto-detected (uwsm -> hyprctl -> none) |
|
||||
| `width` | `600` |
|
||||
| `height` | `400` |
|
||||
| `font_size` | `14` |
|
||||
@@ -141,7 +255,7 @@ uuctl = true
|
||||
|
||||
### Launch Wrapper
|
||||
|
||||
When running in uwsm-managed or Hyprland sessions, owlry auto-detects and uses the appropriate launch wrapper for proper session integration:
|
||||
When running in uwsm-managed or Hyprland sessions, owlry auto-detects and uses the appropriate launch wrapper:
|
||||
|
||||
| Session | Wrapper | Purpose |
|
||||
|---------|---------|---------|
|
||||
@@ -149,23 +263,51 @@ When running in uwsm-managed or Hyprland sessions, owlry auto-detects and uses t
|
||||
| Hyprland | `hyprctl dispatch exec --` | Native Hyprland window management |
|
||||
| Other | None (direct `sh -c`) | Standard shell execution |
|
||||
|
||||
You can override this with `launch_wrapper` in config, or set to empty string `""` to disable.
|
||||
|
||||
## Theming
|
||||
|
||||
### GTK Theme (Default)
|
||||
|
||||
By default, Owlry inherits colors from your system GTK4 theme (Adwaita, Breeze, etc.).
|
||||
|
||||
### Owl Theme
|
||||
### Built-in Themes
|
||||
|
||||
Enable the built-in owl-inspired dark theme:
|
||||
Owlry includes an owl-inspired dark theme:
|
||||
|
||||
```toml
|
||||
[appearance]
|
||||
theme = "owl"
|
||||
```
|
||||
|
||||
### Included Example Themes
|
||||
|
||||
Example themes are installed to `/usr/share/owlry/themes/`:
|
||||
|
||||
| Theme | Description |
|
||||
|-------|-------------|
|
||||
| `owl` | Owl-inspired dark theme with amber accents |
|
||||
| `catppuccin-mocha` | Soothing pastel theme |
|
||||
| `nord` | Arctic, north-bluish palette |
|
||||
| `rose-pine` | All natural pine, faux fur and soho vibes |
|
||||
| `dracula` | Dark theme for vampires |
|
||||
| `gruvbox-dark` | Retro groove color scheme |
|
||||
| `tokyo-night` | Lights of Tokyo at night |
|
||||
| `solarized-dark` | Precision colors for machines and people |
|
||||
| `one-dark` | Atom's iconic One Dark theme |
|
||||
|
||||
To use an example theme:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.config/owlry/themes
|
||||
cp /usr/share/owlry/themes/catppuccin-mocha.css ~/.config/owlry/themes/
|
||||
```
|
||||
|
||||
Then set in config:
|
||||
|
||||
```toml
|
||||
[appearance]
|
||||
theme = "catppuccin-mocha"
|
||||
```
|
||||
|
||||
### Custom Theme
|
||||
|
||||
Create a custom theme file at `~/.config/owlry/themes/mytheme.css`:
|
||||
@@ -182,13 +324,6 @@ Create a custom theme file at `~/.config/owlry/themes/mytheme.css`:
|
||||
}
|
||||
```
|
||||
|
||||
Then reference it in config:
|
||||
|
||||
```toml
|
||||
[appearance]
|
||||
theme = "mytheme"
|
||||
```
|
||||
|
||||
### CSS Variables Reference
|
||||
|
||||
| Variable | Description |
|
||||
@@ -202,35 +337,12 @@ theme = "mytheme"
|
||||
| `--owlry-accent-bright` | Bright accent |
|
||||
| `--owlry-font-size` | Base font size |
|
||||
| `--owlry-border-radius` | Border radius |
|
||||
| `--owlry-badge-app` | Application badge color |
|
||||
| `--owlry-badge-cmd` | Command badge color |
|
||||
| `--owlry-badge-dmenu` | Dmenu badge color |
|
||||
| `--owlry-badge-uuctl` | systemd badge color |
|
||||
| `--owlry-badge-*` | Provider badge colors (app, cmd, sys, ssh, clip, emoji, etc.) |
|
||||
|
||||
### Custom Stylesheet
|
||||
|
||||
For full control, create `~/.config/owlry/style.css` with any GTK4 CSS.
|
||||
|
||||
## Providers
|
||||
|
||||
### Applications
|
||||
|
||||
Searches `.desktop` files from standard XDG directories.
|
||||
|
||||
### Commands
|
||||
|
||||
Searches executable files in `$PATH`.
|
||||
|
||||
### systemd User Services
|
||||
|
||||
Lists and controls user-level systemd services. Select a service to access actions:
|
||||
|
||||
- Start / Stop / Restart / Reload
|
||||
- Kill (force stop)
|
||||
- Status (opens in terminal)
|
||||
- Journal (live logs in terminal)
|
||||
- Enable / Disable (autostart)
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the GNU General Public License v3.0 or later - see [LICENSE](LICENSE) for details.
|
||||
|
||||
77
config.example.toml
Normal file
77
config.example.toml
Normal file
@@ -0,0 +1,77 @@
|
||||
# Owlry Configuration
|
||||
# Copy to ~/.config/owlry/config.toml
|
||||
|
||||
[general]
|
||||
show_icons = true
|
||||
max_results = 10
|
||||
terminal_command = "kitty" # Auto-detected if not set
|
||||
|
||||
# Launch wrapper for app execution (auto-detected if not set)
|
||||
# Examples:
|
||||
# "uwsm app --" # For uwsm sessions
|
||||
# "hyprctl dispatch exec --" # For Hyprland
|
||||
# "" # Direct execution
|
||||
# launch_wrapper = "uwsm app --"
|
||||
|
||||
[appearance]
|
||||
width = 600
|
||||
height = 400
|
||||
font_size = 14
|
||||
border_radius = 12
|
||||
|
||||
# Theme: "owl" for built-in dark theme, or leave unset for GTK default
|
||||
# theme = "owl"
|
||||
|
||||
# Individual color overrides (CSS color values)
|
||||
# [appearance.colors]
|
||||
# background = "#1a1b26"
|
||||
# background_secondary = "#24283b"
|
||||
# border = "#414868"
|
||||
# text = "#c0caf5"
|
||||
# text_secondary = "#565f89"
|
||||
# accent = "#7aa2f7"
|
||||
# accent_bright = "#89b4fa"
|
||||
# badge_app = "#9ece6a"
|
||||
# badge_calc = "#e0af68"
|
||||
# badge_cmd = "#7aa2f7"
|
||||
# badge_dmenu = "#bb9af7"
|
||||
# badge_uuctl = "#f7768e"
|
||||
|
||||
[providers]
|
||||
applications = true
|
||||
commands = true
|
||||
uuctl = true
|
||||
|
||||
# Calculator provider (type "= 5+3" or "calc 5+3")
|
||||
calculator = true
|
||||
|
||||
# Frecency: boost frequently/recently used items in search results
|
||||
frecency = true
|
||||
frecency_weight = 0.3 # 0.0 = disabled, 1.0 = strong boost
|
||||
|
||||
# Web search provider (type "? query" or "web query")
|
||||
websearch = true
|
||||
# Options: google, duckduckgo, bing, startpage, searxng, brave, ecosia
|
||||
# Or custom URL with {query} placeholder, e.g. "https://search.example.com/?q={query}"
|
||||
search_engine = "duckduckgo"
|
||||
|
||||
# System commands (shutdown, reboot, lock, suspend, hibernate, logout, BIOS)
|
||||
system = true
|
||||
|
||||
# SSH connections from ~/.ssh/config
|
||||
ssh = true
|
||||
|
||||
# Clipboard history (requires cliphist)
|
||||
clipboard = true
|
||||
|
||||
# Browser bookmarks (Chrome, Chromium, Brave, Edge, Vivaldi)
|
||||
bookmarks = true
|
||||
|
||||
# Emoji picker (copies to clipboard)
|
||||
emoji = true
|
||||
|
||||
# Custom scripts from ~/.config/owlry/scripts/
|
||||
scripts = true
|
||||
|
||||
# File search (requires fd or locate, trigger with "/ pattern" or "find pattern")
|
||||
files = true
|
||||
@@ -106,6 +106,21 @@
|
||||
color: var(--owlry-badge-app, @blue_3);
|
||||
}
|
||||
|
||||
.owlry-badge-bookmark {
|
||||
background-color: alpha(var(--owlry-badge-bookmark, #f5a623), 0.2);
|
||||
color: var(--owlry-badge-bookmark, #f5a623);
|
||||
}
|
||||
|
||||
.owlry-badge-calc {
|
||||
background-color: alpha(var(--owlry-badge-calc, @yellow_3), 0.2);
|
||||
color: var(--owlry-badge-calc, @yellow_3);
|
||||
}
|
||||
|
||||
.owlry-badge-clip {
|
||||
background-color: alpha(var(--owlry-badge-clip, #8b5cf6), 0.2);
|
||||
color: var(--owlry-badge-clip, #8b5cf6);
|
||||
}
|
||||
|
||||
.owlry-badge-cmd {
|
||||
background-color: alpha(var(--owlry-badge-cmd, @purple_3), 0.2);
|
||||
color: var(--owlry-badge-cmd, @purple_3);
|
||||
@@ -116,11 +131,41 @@
|
||||
color: var(--owlry-badge-dmenu, @green_3);
|
||||
}
|
||||
|
||||
.owlry-badge-emoji {
|
||||
background-color: alpha(var(--owlry-badge-emoji, #f472b6), 0.2);
|
||||
color: var(--owlry-badge-emoji, #f472b6);
|
||||
}
|
||||
|
||||
.owlry-badge-file {
|
||||
background-color: alpha(var(--owlry-badge-file, #22d3ee), 0.2);
|
||||
color: var(--owlry-badge-file, #22d3ee);
|
||||
}
|
||||
|
||||
.owlry-badge-script {
|
||||
background-color: alpha(var(--owlry-badge-script, #a3e635), 0.2);
|
||||
color: var(--owlry-badge-script, #a3e635);
|
||||
}
|
||||
|
||||
.owlry-badge-ssh {
|
||||
background-color: alpha(var(--owlry-badge-ssh, #2dd4bf), 0.2);
|
||||
color: var(--owlry-badge-ssh, #2dd4bf);
|
||||
}
|
||||
|
||||
.owlry-badge-sys {
|
||||
background-color: alpha(var(--owlry-badge-sys, #ef4444), 0.2);
|
||||
color: var(--owlry-badge-sys, #ef4444);
|
||||
}
|
||||
|
||||
.owlry-badge-uuctl {
|
||||
background-color: alpha(var(--owlry-badge-uuctl, @orange_3), 0.2);
|
||||
color: var(--owlry-badge-uuctl, @orange_3);
|
||||
}
|
||||
|
||||
.owlry-badge-web {
|
||||
background-color: alpha(var(--owlry-badge-web, @teal_3), 0.2);
|
||||
color: var(--owlry-badge-web, @teal_3);
|
||||
}
|
||||
|
||||
/* Header bar */
|
||||
.owlry-header {
|
||||
margin-bottom: 4px;
|
||||
@@ -166,22 +211,76 @@
|
||||
border-color: alpha(var(--owlry-badge-app, @blue_3), 0.4);
|
||||
}
|
||||
|
||||
.owlry-filter-bookmark:checked {
|
||||
background-color: alpha(var(--owlry-badge-bookmark, #f5a623), 0.2);
|
||||
color: var(--owlry-badge-bookmark, #f5a623);
|
||||
border-color: alpha(var(--owlry-badge-bookmark, #f5a623), 0.4);
|
||||
}
|
||||
|
||||
.owlry-filter-calc:checked {
|
||||
background-color: alpha(var(--owlry-badge-calc, @yellow_3), 0.2);
|
||||
color: var(--owlry-badge-calc, @yellow_3);
|
||||
border-color: alpha(var(--owlry-badge-calc, @yellow_3), 0.4);
|
||||
}
|
||||
|
||||
.owlry-filter-clip:checked {
|
||||
background-color: alpha(var(--owlry-badge-clip, #8b5cf6), 0.2);
|
||||
color: var(--owlry-badge-clip, #8b5cf6);
|
||||
border-color: alpha(var(--owlry-badge-clip, #8b5cf6), 0.4);
|
||||
}
|
||||
|
||||
.owlry-filter-cmd:checked {
|
||||
background-color: alpha(var(--owlry-badge-cmd, @purple_3), 0.2);
|
||||
color: var(--owlry-badge-cmd, @purple_3);
|
||||
border-color: alpha(var(--owlry-badge-cmd, @purple_3), 0.4);
|
||||
}
|
||||
|
||||
.owlry-filter-dmenu:checked {
|
||||
background-color: alpha(var(--owlry-badge-dmenu, @green_3), 0.2);
|
||||
color: var(--owlry-badge-dmenu, @green_3);
|
||||
border-color: alpha(var(--owlry-badge-dmenu, @green_3), 0.4);
|
||||
}
|
||||
|
||||
.owlry-filter-emoji:checked {
|
||||
background-color: alpha(var(--owlry-badge-emoji, #f472b6), 0.2);
|
||||
color: var(--owlry-badge-emoji, #f472b6);
|
||||
border-color: alpha(var(--owlry-badge-emoji, #f472b6), 0.4);
|
||||
}
|
||||
|
||||
.owlry-filter-file:checked {
|
||||
background-color: alpha(var(--owlry-badge-file, #22d3ee), 0.2);
|
||||
color: var(--owlry-badge-file, #22d3ee);
|
||||
border-color: alpha(var(--owlry-badge-file, #22d3ee), 0.4);
|
||||
}
|
||||
|
||||
.owlry-filter-script:checked {
|
||||
background-color: alpha(var(--owlry-badge-script, #a3e635), 0.2);
|
||||
color: var(--owlry-badge-script, #a3e635);
|
||||
border-color: alpha(var(--owlry-badge-script, #a3e635), 0.4);
|
||||
}
|
||||
|
||||
.owlry-filter-ssh:checked {
|
||||
background-color: alpha(var(--owlry-badge-ssh, #2dd4bf), 0.2);
|
||||
color: var(--owlry-badge-ssh, #2dd4bf);
|
||||
border-color: alpha(var(--owlry-badge-ssh, #2dd4bf), 0.4);
|
||||
}
|
||||
|
||||
.owlry-filter-sys:checked {
|
||||
background-color: alpha(var(--owlry-badge-sys, #ef4444), 0.2);
|
||||
color: var(--owlry-badge-sys, #ef4444);
|
||||
border-color: alpha(var(--owlry-badge-sys, #ef4444), 0.4);
|
||||
}
|
||||
|
||||
.owlry-filter-uuctl:checked {
|
||||
background-color: alpha(var(--owlry-badge-uuctl, @orange_3), 0.2);
|
||||
color: var(--owlry-badge-uuctl, @orange_3);
|
||||
border-color: alpha(var(--owlry-badge-uuctl, @orange_3), 0.4);
|
||||
}
|
||||
|
||||
.owlry-filter-dmenu:checked {
|
||||
background-color: alpha(var(--owlry-badge-dmenu, @green_3), 0.2);
|
||||
color: var(--owlry-badge-dmenu, @green_3);
|
||||
border-color: alpha(var(--owlry-badge-dmenu, @green_3), 0.4);
|
||||
.owlry-filter-web:checked {
|
||||
background-color: alpha(var(--owlry-badge-web, @teal_3), 0.2);
|
||||
color: var(--owlry-badge-web, @teal_3);
|
||||
border-color: alpha(var(--owlry-badge-web, @teal_3), 0.4);
|
||||
}
|
||||
|
||||
/* Hints bar at bottom */
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::cli::CliArgs;
|
||||
use crate::config::Config;
|
||||
use crate::data::FrecencyStore;
|
||||
use crate::filter::ProviderFilter;
|
||||
use crate::providers::ProviderManager;
|
||||
use crate::theme;
|
||||
@@ -39,7 +40,10 @@ impl OwlryApp {
|
||||
debug!("Activating Owlry");
|
||||
|
||||
let config = Rc::new(RefCell::new(Config::load_or_default()));
|
||||
let providers = Rc::new(RefCell::new(ProviderManager::new()));
|
||||
let search_engine = config.borrow().providers.search_engine.clone();
|
||||
let terminal = config.borrow().general.terminal_command.clone();
|
||||
let providers = Rc::new(RefCell::new(ProviderManager::with_config(&search_engine, &terminal)));
|
||||
let frecency = Rc::new(RefCell::new(FrecencyStore::load_or_default()));
|
||||
|
||||
// Create filter from CLI args and config
|
||||
let filter = ProviderFilter::new(
|
||||
@@ -49,7 +53,7 @@ impl OwlryApp {
|
||||
);
|
||||
let filter = Rc::new(RefCell::new(filter));
|
||||
|
||||
let window = MainWindow::new(app, config.clone(), providers.clone(), filter.clone());
|
||||
let window = MainWindow::new(app, config.clone(), providers.clone(), frecency.clone(), filter.clone());
|
||||
|
||||
// Set up layer shell for Wayland overlay behavior
|
||||
window.init_layer_shell();
|
||||
|
||||
@@ -35,9 +35,18 @@ pub struct ThemeColors {
|
||||
pub accent_bright: Option<String>,
|
||||
// Provider badge colors
|
||||
pub badge_app: Option<String>,
|
||||
pub badge_bookmark: Option<String>,
|
||||
pub badge_calc: Option<String>,
|
||||
pub badge_clip: Option<String>,
|
||||
pub badge_cmd: Option<String>,
|
||||
pub badge_dmenu: Option<String>,
|
||||
pub badge_emoji: Option<String>,
|
||||
pub badge_file: Option<String>,
|
||||
pub badge_script: Option<String>,
|
||||
pub badge_ssh: Option<String>,
|
||||
pub badge_sys: Option<String>,
|
||||
pub badge_uuctl: Option<String>,
|
||||
pub badge_web: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -59,6 +68,56 @@ pub struct ProvidersConfig {
|
||||
pub applications: bool,
|
||||
pub commands: bool,
|
||||
pub uuctl: bool,
|
||||
/// Enable calculator provider (= expression or calc expression)
|
||||
#[serde(default = "default_true")]
|
||||
pub calculator: bool,
|
||||
/// Enable frecency-based result ranking
|
||||
#[serde(default = "default_true")]
|
||||
pub frecency: bool,
|
||||
/// Weight for frecency boost (0.0 = disabled, 1.0 = strong boost)
|
||||
#[serde(default = "default_frecency_weight")]
|
||||
pub frecency_weight: f64,
|
||||
/// Enable web search provider (? query or web query)
|
||||
#[serde(default = "default_true")]
|
||||
pub websearch: bool,
|
||||
/// Search engine for web search
|
||||
/// Options: google, duckduckgo, bing, startpage, searxng, brave, ecosia
|
||||
/// Or custom URL with {query} placeholder
|
||||
#[serde(default = "default_search_engine")]
|
||||
pub search_engine: String,
|
||||
/// Enable system commands (shutdown, reboot, etc.)
|
||||
#[serde(default = "default_true")]
|
||||
pub system: bool,
|
||||
/// Enable SSH connections from ~/.ssh/config
|
||||
#[serde(default = "default_true")]
|
||||
pub ssh: bool,
|
||||
/// Enable clipboard history (requires cliphist)
|
||||
#[serde(default = "default_true")]
|
||||
pub clipboard: bool,
|
||||
/// Enable browser bookmarks
|
||||
#[serde(default = "default_true")]
|
||||
pub bookmarks: bool,
|
||||
/// Enable emoji picker
|
||||
#[serde(default = "default_true")]
|
||||
pub emoji: bool,
|
||||
/// Enable custom scripts from ~/.config/owlry/scripts/
|
||||
#[serde(default = "default_true")]
|
||||
pub scripts: bool,
|
||||
/// Enable file search (requires fd or locate)
|
||||
#[serde(default = "default_true")]
|
||||
pub files: bool,
|
||||
}
|
||||
|
||||
fn default_search_engine() -> String {
|
||||
"duckduckgo".to_string()
|
||||
}
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_frecency_weight() -> f64 {
|
||||
0.3
|
||||
}
|
||||
|
||||
/// Detect the best launch wrapper for the current session
|
||||
@@ -172,6 +231,18 @@ impl Default for Config {
|
||||
applications: true,
|
||||
commands: true,
|
||||
uuctl: true,
|
||||
calculator: true,
|
||||
frecency: true,
|
||||
frecency_weight: 0.3,
|
||||
websearch: true,
|
||||
search_engine: "duckduckgo".to_string(),
|
||||
system: true,
|
||||
ssh: true,
|
||||
clipboard: true,
|
||||
bookmarks: true,
|
||||
emoji: true,
|
||||
scripts: true,
|
||||
files: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
223
src/data/frecency.rs
Normal file
223
src/data/frecency.rs
Normal file
@@ -0,0 +1,223 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use log::{debug, info, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// A single frecency entry tracking launch count and recency
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FrecencyEntry {
|
||||
pub launch_count: u32,
|
||||
pub last_launch: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Persistent frecency data store
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FrecencyData {
|
||||
pub version: u32,
|
||||
pub entries: HashMap<String, FrecencyEntry>,
|
||||
}
|
||||
|
||||
impl Default for FrecencyData {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
version: 1,
|
||||
entries: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Frecency store for tracking and boosting recently/frequently used items
|
||||
pub struct FrecencyStore {
|
||||
data: FrecencyData,
|
||||
path: PathBuf,
|
||||
dirty: bool,
|
||||
}
|
||||
|
||||
impl FrecencyStore {
|
||||
/// Create a new frecency store, loading existing data if available
|
||||
pub fn new() -> Self {
|
||||
let path = Self::data_path();
|
||||
let data = Self::load_from_path(&path).unwrap_or_default();
|
||||
|
||||
info!("Frecency store loaded with {} entries", data.entries.len());
|
||||
|
||||
Self {
|
||||
data,
|
||||
path,
|
||||
dirty: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Alias for new() - loads from disk or creates default
|
||||
pub fn load_or_default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
|
||||
/// Get the path to the frecency data file
|
||||
fn data_path() -> PathBuf {
|
||||
dirs::data_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join("owlry")
|
||||
.join("frecency.json")
|
||||
}
|
||||
|
||||
/// Load frecency data from a file
|
||||
fn load_from_path(path: &PathBuf) -> Option<FrecencyData> {
|
||||
if !path.exists() {
|
||||
debug!("Frecency file not found at {:?}", path);
|
||||
return None;
|
||||
}
|
||||
|
||||
let content = std::fs::read_to_string(path).ok()?;
|
||||
match serde_json::from_str(&content) {
|
||||
Ok(data) => Some(data),
|
||||
Err(e) => {
|
||||
warn!("Failed to parse frecency data: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Save frecency data to disk
|
||||
pub fn save(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
if !self.dirty {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Ensure directory exists
|
||||
if let Some(parent) = self.path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
let content = serde_json::to_string_pretty(&self.data)?;
|
||||
std::fs::write(&self.path, content)?;
|
||||
self.dirty = false;
|
||||
|
||||
debug!("Frecency data saved to {:?}", self.path);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Record a launch event for an item
|
||||
pub fn record_launch(&mut self, item_id: &str) {
|
||||
let now = Utc::now();
|
||||
|
||||
let entry = self
|
||||
.data
|
||||
.entries
|
||||
.entry(item_id.to_string())
|
||||
.or_insert(FrecencyEntry {
|
||||
launch_count: 0,
|
||||
last_launch: now,
|
||||
});
|
||||
|
||||
entry.launch_count += 1;
|
||||
entry.last_launch = now;
|
||||
self.dirty = true;
|
||||
|
||||
debug!(
|
||||
"Recorded launch for '{}': count={}, last={}",
|
||||
item_id, entry.launch_count, entry.last_launch
|
||||
);
|
||||
|
||||
// Auto-save after recording
|
||||
if let Err(e) = self.save() {
|
||||
warn!("Failed to save frecency data: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate frecency score for an item
|
||||
/// Uses Firefox-style algorithm: score = launch_count * recency_weight
|
||||
pub fn get_score(&self, item_id: &str) -> f64 {
|
||||
match self.data.entries.get(item_id) {
|
||||
Some(entry) => Self::calculate_frecency(entry.launch_count, entry.last_launch),
|
||||
None => 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate frecency using Firefox-style algorithm
|
||||
fn calculate_frecency(launch_count: u32, last_launch: DateTime<Utc>) -> f64 {
|
||||
let now = Utc::now();
|
||||
let age = now.signed_duration_since(last_launch);
|
||||
let age_days = age.num_hours() as f64 / 24.0;
|
||||
|
||||
// Recency weight based on how recently the item was used
|
||||
let recency_weight = if age_days < 1.0 {
|
||||
100.0 // Today
|
||||
} else if age_days < 7.0 {
|
||||
70.0 // This week
|
||||
} else if age_days < 30.0 {
|
||||
50.0 // This month
|
||||
} else if age_days < 90.0 {
|
||||
30.0 // This quarter
|
||||
} else {
|
||||
10.0 // Older
|
||||
};
|
||||
|
||||
launch_count as f64 * recency_weight
|
||||
}
|
||||
|
||||
/// Get all entries (for debugging/display)
|
||||
#[allow(dead_code)]
|
||||
pub fn entries(&self) -> &HashMap<String, FrecencyEntry> {
|
||||
&self.data.entries
|
||||
}
|
||||
|
||||
/// Clear all frecency data
|
||||
#[allow(dead_code)]
|
||||
pub fn clear(&mut self) {
|
||||
self.data.entries.clear();
|
||||
self.dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FrecencyStore {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for FrecencyStore {
|
||||
fn drop(&mut self) {
|
||||
// Attempt to save on drop
|
||||
if let Err(e) = self.save() {
|
||||
warn!("Failed to save frecency data on drop: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_frecency_calculation() {
|
||||
let now = Utc::now();
|
||||
|
||||
// Recent launch should have high score
|
||||
let score_today = FrecencyStore::calculate_frecency(10, now);
|
||||
assert!(score_today > 900.0); // 10 * 100
|
||||
|
||||
// Older launch should have lower score
|
||||
let week_ago = now - chrono::Duration::days(5);
|
||||
let score_week = FrecencyStore::calculate_frecency(10, week_ago);
|
||||
assert!(score_week < score_today);
|
||||
assert!(score_week > 600.0); // 10 * 70
|
||||
|
||||
// Much older launch
|
||||
let month_ago = now - chrono::Duration::days(45);
|
||||
let score_month = FrecencyStore::calculate_frecency(10, month_ago);
|
||||
assert!(score_month < score_week);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_launch_count_matters() {
|
||||
let now = Utc::now();
|
||||
|
||||
let score_few = FrecencyStore::calculate_frecency(2, now);
|
||||
let score_many = FrecencyStore::calculate_frecency(20, now);
|
||||
|
||||
assert!(score_many > score_few);
|
||||
assert!((score_many / score_few - 10.0).abs() < 0.1); // Should be ~10x
|
||||
}
|
||||
}
|
||||
3
src/data/mod.rs
Normal file
3
src/data/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
mod frecency;
|
||||
|
||||
pub use frecency::FrecencyStore;
|
||||
@@ -42,6 +42,26 @@ impl ProviderFilter {
|
||||
if config_providers.uuctl {
|
||||
set.insert(ProviderType::Uuctl);
|
||||
}
|
||||
if config_providers.system {
|
||||
set.insert(ProviderType::System);
|
||||
}
|
||||
if config_providers.ssh {
|
||||
set.insert(ProviderType::Ssh);
|
||||
}
|
||||
if config_providers.clipboard {
|
||||
set.insert(ProviderType::Clipboard);
|
||||
}
|
||||
if config_providers.bookmarks {
|
||||
set.insert(ProviderType::Bookmarks);
|
||||
}
|
||||
if config_providers.emoji {
|
||||
set.insert(ProviderType::Emoji);
|
||||
}
|
||||
if config_providers.scripts {
|
||||
set.insert(ProviderType::Scripts);
|
||||
}
|
||||
// Note: Files, Calculator, WebSearch are dynamic providers
|
||||
// that don't need to be in the filter set - they're triggered by prefix
|
||||
// Default to apps if nothing enabled
|
||||
if set.is_empty() {
|
||||
set.insert(ProviderType::Application);
|
||||
@@ -129,9 +149,29 @@ impl ProviderFilter {
|
||||
let prefixes = [
|
||||
(":app ", ProviderType::Application),
|
||||
(":apps ", ProviderType::Application),
|
||||
(":bm ", ProviderType::Bookmarks),
|
||||
(":bookmark ", ProviderType::Bookmarks),
|
||||
(":bookmarks ", ProviderType::Bookmarks),
|
||||
(":calc ", ProviderType::Calculator),
|
||||
(":calculator ", ProviderType::Calculator),
|
||||
(":clip ", ProviderType::Clipboard),
|
||||
(":clipboard ", ProviderType::Clipboard),
|
||||
(":cmd ", ProviderType::Command),
|
||||
(":command ", ProviderType::Command),
|
||||
(":emoji ", ProviderType::Emoji),
|
||||
(":emojis ", ProviderType::Emoji),
|
||||
(":file ", ProviderType::Files),
|
||||
(":files ", ProviderType::Files),
|
||||
(":find ", ProviderType::Files),
|
||||
(":script ", ProviderType::Scripts),
|
||||
(":scripts ", ProviderType::Scripts),
|
||||
(":ssh ", ProviderType::Ssh),
|
||||
(":sys ", ProviderType::System),
|
||||
(":system ", ProviderType::System),
|
||||
(":power ", ProviderType::System),
|
||||
(":uuctl ", ProviderType::Uuctl),
|
||||
(":web ", ProviderType::WebSearch),
|
||||
(":search ", ProviderType::WebSearch),
|
||||
];
|
||||
|
||||
for (prefix_str, provider) in prefixes {
|
||||
@@ -147,9 +187,29 @@ impl ProviderFilter {
|
||||
let partial_prefixes = [
|
||||
(":app", ProviderType::Application),
|
||||
(":apps", ProviderType::Application),
|
||||
(":bm", ProviderType::Bookmarks),
|
||||
(":bookmark", ProviderType::Bookmarks),
|
||||
(":bookmarks", ProviderType::Bookmarks),
|
||||
(":calc", ProviderType::Calculator),
|
||||
(":calculator", ProviderType::Calculator),
|
||||
(":clip", ProviderType::Clipboard),
|
||||
(":clipboard", ProviderType::Clipboard),
|
||||
(":cmd", ProviderType::Command),
|
||||
(":command", ProviderType::Command),
|
||||
(":emoji", ProviderType::Emoji),
|
||||
(":emojis", ProviderType::Emoji),
|
||||
(":file", ProviderType::Files),
|
||||
(":files", ProviderType::Files),
|
||||
(":find", ProviderType::Files),
|
||||
(":script", ProviderType::Scripts),
|
||||
(":scripts", ProviderType::Scripts),
|
||||
(":ssh", ProviderType::Ssh),
|
||||
(":sys", ProviderType::System),
|
||||
(":system", ProviderType::System),
|
||||
(":power", ProviderType::System),
|
||||
(":uuctl", ProviderType::Uuctl),
|
||||
(":web", ProviderType::WebSearch),
|
||||
(":search", ProviderType::WebSearch),
|
||||
];
|
||||
|
||||
for (prefix_str, provider) in partial_prefixes {
|
||||
@@ -172,9 +232,18 @@ impl ProviderFilter {
|
||||
let mut providers: Vec<_> = self.enabled.iter().copied().collect();
|
||||
providers.sort_by_key(|p| match p {
|
||||
ProviderType::Application => 0,
|
||||
ProviderType::Command => 1,
|
||||
ProviderType::Uuctl => 2,
|
||||
ProviderType::Dmenu => 3,
|
||||
ProviderType::Bookmarks => 1,
|
||||
ProviderType::Calculator => 2,
|
||||
ProviderType::Clipboard => 3,
|
||||
ProviderType::Command => 4,
|
||||
ProviderType::Dmenu => 5,
|
||||
ProviderType::Emoji => 6,
|
||||
ProviderType::Files => 7,
|
||||
ProviderType::Scripts => 8,
|
||||
ProviderType::Ssh => 9,
|
||||
ProviderType::System => 10,
|
||||
ProviderType::Uuctl => 11,
|
||||
ProviderType::WebSearch => 12,
|
||||
});
|
||||
providers
|
||||
}
|
||||
@@ -184,9 +253,18 @@ impl ProviderFilter {
|
||||
if let Some(prefix) = self.active_prefix {
|
||||
return match prefix {
|
||||
ProviderType::Application => "Apps",
|
||||
ProviderType::Bookmarks => "Bookmarks",
|
||||
ProviderType::Calculator => "Calc",
|
||||
ProviderType::Clipboard => "Clipboard",
|
||||
ProviderType::Command => "Commands",
|
||||
ProviderType::Uuctl => "uuctl",
|
||||
ProviderType::Dmenu => "dmenu",
|
||||
ProviderType::Emoji => "Emoji",
|
||||
ProviderType::Files => "Files",
|
||||
ProviderType::Scripts => "Scripts",
|
||||
ProviderType::Ssh => "SSH",
|
||||
ProviderType::System => "System",
|
||||
ProviderType::Uuctl => "uuctl",
|
||||
ProviderType::WebSearch => "Web",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -194,9 +272,18 @@ impl ProviderFilter {
|
||||
if enabled.len() == 1 {
|
||||
match enabled[0] {
|
||||
ProviderType::Application => "Apps",
|
||||
ProviderType::Bookmarks => "Bookmarks",
|
||||
ProviderType::Calculator => "Calc",
|
||||
ProviderType::Clipboard => "Clipboard",
|
||||
ProviderType::Command => "Commands",
|
||||
ProviderType::Uuctl => "uuctl",
|
||||
ProviderType::Dmenu => "dmenu",
|
||||
ProviderType::Emoji => "Emoji",
|
||||
ProviderType::Files => "Files",
|
||||
ProviderType::Scripts => "Scripts",
|
||||
ProviderType::Ssh => "SSH",
|
||||
ProviderType::System => "System",
|
||||
ProviderType::Uuctl => "uuctl",
|
||||
ProviderType::WebSearch => "Web",
|
||||
}
|
||||
} else {
|
||||
"All"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
mod app;
|
||||
mod cli;
|
||||
mod config;
|
||||
mod data;
|
||||
mod filter;
|
||||
mod providers;
|
||||
mod theme;
|
||||
|
||||
242
src/providers/bookmarks.rs
Normal file
242
src/providers/bookmarks.rs
Normal file
@@ -0,0 +1,242 @@
|
||||
use crate::providers::{LaunchItem, Provider, ProviderType};
|
||||
use log::{debug, warn};
|
||||
use serde::Deserialize;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Browser bookmarks provider - reads Firefox and Chrome bookmarks
|
||||
pub struct BookmarksProvider {
|
||||
items: Vec<LaunchItem>,
|
||||
}
|
||||
|
||||
impl BookmarksProvider {
|
||||
pub fn new() -> Self {
|
||||
Self { items: Vec::new() }
|
||||
}
|
||||
|
||||
fn load_bookmarks(&mut self) {
|
||||
self.items.clear();
|
||||
|
||||
// Try Firefox first, then Chrome/Chromium
|
||||
self.load_firefox_bookmarks();
|
||||
self.load_chrome_bookmarks();
|
||||
|
||||
debug!("Loaded {} bookmarks total", self.items.len());
|
||||
}
|
||||
|
||||
fn load_firefox_bookmarks(&mut self) {
|
||||
// Firefox stores bookmarks in places.sqlite
|
||||
// The file is locked when Firefox is running, so we read from backup
|
||||
let firefox_dir = match dirs::home_dir() {
|
||||
Some(h) => h.join(".mozilla").join("firefox"),
|
||||
None => return,
|
||||
};
|
||||
|
||||
if !firefox_dir.exists() {
|
||||
debug!("Firefox directory not found");
|
||||
return;
|
||||
}
|
||||
|
||||
// Find default profile (ends with .default-release or .default)
|
||||
let profile_dir = match Self::find_firefox_profile(&firefox_dir) {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
debug!("No Firefox profile found");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Try to read bookmarkbackups (JSON format, not locked)
|
||||
let backup_dir = profile_dir.join("bookmarkbackups");
|
||||
if backup_dir.exists() {
|
||||
if let Some(latest_backup) = Self::find_latest_file(&backup_dir, "jsonlz4") {
|
||||
// jsonlz4 files need decompression - skip for now, try places.sqlite
|
||||
debug!("Found Firefox backup at {:?}, but jsonlz4 not supported", latest_backup);
|
||||
}
|
||||
}
|
||||
|
||||
// Try places.sqlite directly (may fail if Firefox is running)
|
||||
let places_db = profile_dir.join("places.sqlite");
|
||||
if places_db.exists() {
|
||||
self.read_firefox_places(&places_db);
|
||||
}
|
||||
}
|
||||
|
||||
fn find_firefox_profile(firefox_dir: &PathBuf) -> Option<PathBuf> {
|
||||
let entries = fs::read_dir(firefox_dir).ok()?;
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
if name.ends_with(".default-release") || name.ends_with(".default") {
|
||||
return Some(entry.path());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn find_latest_file(dir: &PathBuf, extension: &str) -> Option<PathBuf> {
|
||||
let entries = fs::read_dir(dir).ok()?;
|
||||
|
||||
entries
|
||||
.flatten()
|
||||
.filter(|e| {
|
||||
e.path()
|
||||
.extension()
|
||||
.map(|ext| ext == extension)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.max_by_key(|e| e.metadata().ok().and_then(|m| m.modified().ok()))
|
||||
.map(|e| e.path())
|
||||
}
|
||||
|
||||
fn read_firefox_places(&mut self, db_path: &PathBuf) {
|
||||
// Note: This requires the rusqlite crate which we don't have
|
||||
// For now, skip Firefox SQLite reading
|
||||
debug!(
|
||||
"Firefox places.sqlite found at {:?}, but SQLite reading not implemented",
|
||||
db_path
|
||||
);
|
||||
}
|
||||
|
||||
fn load_chrome_bookmarks(&mut self) {
|
||||
// Chrome/Chromium bookmarks are in JSON format
|
||||
let home = match dirs::home_dir() {
|
||||
Some(h) => h,
|
||||
None => return,
|
||||
};
|
||||
|
||||
// Try multiple browser paths
|
||||
let bookmark_paths = [
|
||||
// Chrome
|
||||
home.join(".config/google-chrome/Default/Bookmarks"),
|
||||
// Chromium
|
||||
home.join(".config/chromium/Default/Bookmarks"),
|
||||
// Brave
|
||||
home.join(".config/BraveSoftware/Brave-Browser/Default/Bookmarks"),
|
||||
// Edge
|
||||
home.join(".config/microsoft-edge/Default/Bookmarks"),
|
||||
// Vivaldi
|
||||
home.join(".config/vivaldi/Default/Bookmarks"),
|
||||
];
|
||||
|
||||
for path in &bookmark_paths {
|
||||
if path.exists() {
|
||||
self.read_chrome_bookmarks(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_chrome_bookmarks(&mut self, path: &PathBuf) {
|
||||
let content = match fs::read_to_string(path) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
warn!("Failed to read Chrome bookmarks from {:?}: {}", path, e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let bookmarks: ChromeBookmarks = match serde_json::from_str(&content) {
|
||||
Ok(b) => b,
|
||||
Err(e) => {
|
||||
warn!("Failed to parse Chrome bookmarks: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Process bookmark bar and other folders
|
||||
if let Some(roots) = bookmarks.roots {
|
||||
if let Some(bar) = roots.bookmark_bar {
|
||||
self.process_chrome_folder(&bar);
|
||||
}
|
||||
if let Some(other) = roots.other {
|
||||
self.process_chrome_folder(&other);
|
||||
}
|
||||
if let Some(synced) = roots.synced {
|
||||
self.process_chrome_folder(&synced);
|
||||
}
|
||||
}
|
||||
|
||||
debug!("Loaded Chrome bookmarks from {:?}", path);
|
||||
}
|
||||
|
||||
fn process_chrome_folder(&mut self, folder: &ChromeBookmarkNode) {
|
||||
if let Some(ref children) = folder.children {
|
||||
for child in children {
|
||||
match child.node_type.as_deref() {
|
||||
Some("url") => {
|
||||
if let Some(ref url) = child.url {
|
||||
let name = child.name.clone().unwrap_or_else(|| url.clone());
|
||||
|
||||
self.items.push(LaunchItem {
|
||||
id: format!("bookmark:{}", url),
|
||||
name,
|
||||
description: Some(url.clone()),
|
||||
icon: Some("web-browser".to_string()),
|
||||
provider: ProviderType::Bookmarks,
|
||||
command: format!("xdg-open '{}'", url.replace('\'', "'\\''")),
|
||||
terminal: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
Some("folder") => {
|
||||
// Recursively process subfolders
|
||||
self.process_chrome_folder(child);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Chrome bookmark JSON structures
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ChromeBookmarks {
|
||||
roots: Option<ChromeBookmarkRoots>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ChromeBookmarkRoots {
|
||||
bookmark_bar: Option<ChromeBookmarkNode>,
|
||||
other: Option<ChromeBookmarkNode>,
|
||||
synced: Option<ChromeBookmarkNode>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ChromeBookmarkNode {
|
||||
name: Option<String>,
|
||||
url: Option<String>,
|
||||
#[serde(rename = "type")]
|
||||
node_type: Option<String>,
|
||||
children: Option<Vec<ChromeBookmarkNode>>,
|
||||
}
|
||||
|
||||
impl Provider for BookmarksProvider {
|
||||
fn name(&self) -> &str {
|
||||
"Bookmarks"
|
||||
}
|
||||
|
||||
fn provider_type(&self) -> ProviderType {
|
||||
ProviderType::Bookmarks
|
||||
}
|
||||
|
||||
fn refresh(&mut self) {
|
||||
self.load_bookmarks();
|
||||
}
|
||||
|
||||
fn items(&self) -> &[LaunchItem] {
|
||||
&self.items
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_bookmarks_provider() {
|
||||
let mut provider = BookmarksProvider::new();
|
||||
provider.refresh();
|
||||
// Just ensure it doesn't panic
|
||||
}
|
||||
}
|
||||
237
src/providers/calculator.rs
Normal file
237
src/providers/calculator.rs
Normal file
@@ -0,0 +1,237 @@
|
||||
use super::{LaunchItem, Provider, ProviderType};
|
||||
use log::debug;
|
||||
|
||||
/// Calculator provider for evaluating math expressions
|
||||
/// Syntax: `= expression` or `calc expression`
|
||||
pub struct CalculatorProvider {
|
||||
/// Cached result from last evaluation
|
||||
cached_result: Option<LaunchItem>,
|
||||
}
|
||||
|
||||
impl CalculatorProvider {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
cached_result: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a query is a calculator expression
|
||||
pub fn is_calculator_query(query: &str) -> bool {
|
||||
let trimmed = query.trim();
|
||||
trimmed.starts_with("=") || trimmed.starts_with("calc ")
|
||||
}
|
||||
|
||||
/// Extract the expression from a calculator query
|
||||
fn extract_expression(query: &str) -> Option<&str> {
|
||||
let trimmed = query.trim();
|
||||
// Support both "= expr" and "=expr" (with or without space)
|
||||
if let Some(expr) = trimmed.strip_prefix("= ") {
|
||||
Some(expr.trim())
|
||||
} else if let Some(expr) = trimmed.strip_prefix("=") {
|
||||
Some(expr.trim())
|
||||
} else if let Some(expr) = trimmed.strip_prefix("calc ") {
|
||||
Some(expr.trim())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if string looks like a math expression (for :calc mode)
|
||||
pub fn looks_like_expression(query: &str) -> bool {
|
||||
let trimmed = query.trim();
|
||||
if trimmed.is_empty() {
|
||||
return false;
|
||||
}
|
||||
// Contains math operators or is a number
|
||||
trimmed.chars().any(|c| "+-*/^()".contains(c))
|
||||
|| trimmed.parse::<f64>().is_ok()
|
||||
|| ["pi", "e", "sqrt", "sin", "cos", "tan", "abs", "ln", "log"]
|
||||
.iter()
|
||||
.any(|f| trimmed.to_lowercase().contains(f))
|
||||
}
|
||||
|
||||
/// Evaluate a raw expression (for :calc filter mode)
|
||||
pub fn evaluate_raw(&mut self, expr: &str) -> Option<LaunchItem> {
|
||||
let trimmed = expr.trim();
|
||||
if trimmed.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
match meval::eval_str(trimmed) {
|
||||
Ok(result) => {
|
||||
let result_str = if result.fract() == 0.0 && result.abs() < 1e15 {
|
||||
format!("{}", result as i64)
|
||||
} else {
|
||||
format!("{:.10}", result).trim_end_matches('0').trim_end_matches('.').to_string()
|
||||
};
|
||||
|
||||
Some(LaunchItem {
|
||||
id: format!("calc:{}", trimmed),
|
||||
name: format!("{} = {}", trimmed, result_str),
|
||||
description: Some("Press Enter to copy result".to_string()),
|
||||
icon: Some("accessories-calculator".to_string()),
|
||||
provider: ProviderType::Calculator,
|
||||
command: format!("echo -n '{}' | wl-copy", result_str),
|
||||
terminal: false,
|
||||
})
|
||||
}
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluate an expression and return a LaunchItem result
|
||||
pub fn evaluate(&mut self, query: &str) -> Option<LaunchItem> {
|
||||
let expr = Self::extract_expression(query)?;
|
||||
|
||||
if expr.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
debug!("Evaluating expression: {}", expr);
|
||||
|
||||
match meval::eval_str(expr) {
|
||||
Ok(result) => {
|
||||
// Format result nicely
|
||||
let result_str = if result.fract() == 0.0 && result.abs() < 1e15 {
|
||||
// Integer result
|
||||
format!("{}", result as i64)
|
||||
} else {
|
||||
// Float result with reasonable precision
|
||||
let formatted = format!("{:.10}", result);
|
||||
// Trim trailing zeros
|
||||
formatted.trim_end_matches('0').trim_end_matches('.').to_string()
|
||||
};
|
||||
|
||||
let item = LaunchItem {
|
||||
id: format!("calc:{}", expr),
|
||||
name: result_str.clone(),
|
||||
description: Some(format!("= {}", expr)),
|
||||
icon: Some("accessories-calculator".to_string()),
|
||||
provider: ProviderType::Calculator,
|
||||
// Copy result to clipboard using wl-copy
|
||||
command: format!("sh -c 'echo -n \"{}\" | wl-copy'", result_str),
|
||||
terminal: false,
|
||||
};
|
||||
|
||||
debug!("Calculator result: {} = {}", expr, result_str);
|
||||
self.cached_result = Some(item.clone());
|
||||
Some(item)
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("Calculator error for '{}': {}", expr, e);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Provider for CalculatorProvider {
|
||||
fn name(&self) -> &str {
|
||||
"Calculator"
|
||||
}
|
||||
|
||||
fn provider_type(&self) -> ProviderType {
|
||||
ProviderType::Calculator
|
||||
}
|
||||
|
||||
fn refresh(&mut self) {
|
||||
// Calculator doesn't need refresh - it evaluates on-demand
|
||||
self.cached_result = None;
|
||||
}
|
||||
|
||||
fn items(&self) -> &[LaunchItem] {
|
||||
// Calculator is a dynamic provider - items are generated from query
|
||||
// Return cached result if available (for UI display)
|
||||
match &self.cached_result {
|
||||
Some(item) => std::slice::from_ref(item),
|
||||
None => &[],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_is_calculator_query() {
|
||||
assert!(CalculatorProvider::is_calculator_query("= 5+3"));
|
||||
assert!(CalculatorProvider::is_calculator_query("calc 5+3"));
|
||||
assert!(CalculatorProvider::is_calculator_query(" = 5+3"));
|
||||
assert!(!CalculatorProvider::is_calculator_query("5+3"));
|
||||
assert!(!CalculatorProvider::is_calculator_query("firefox"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_expression() {
|
||||
assert_eq!(
|
||||
CalculatorProvider::extract_expression("= 5+3"),
|
||||
Some("5+3")
|
||||
);
|
||||
assert_eq!(
|
||||
CalculatorProvider::extract_expression("calc 5+3"),
|
||||
Some("5+3")
|
||||
);
|
||||
assert_eq!(
|
||||
CalculatorProvider::extract_expression("= 5 + 3 "),
|
||||
Some("5 + 3")
|
||||
);
|
||||
assert_eq!(CalculatorProvider::extract_expression("5+3"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_evaluate_basic() {
|
||||
let mut calc = CalculatorProvider::new();
|
||||
|
||||
let result = calc.evaluate("= 5+3").unwrap();
|
||||
assert_eq!(result.name, "8");
|
||||
|
||||
let result = calc.evaluate("= 10 * 2").unwrap();
|
||||
assert_eq!(result.name, "20");
|
||||
|
||||
let result = calc.evaluate("= 15 / 3").unwrap();
|
||||
assert_eq!(result.name, "5");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_evaluate_float() {
|
||||
let mut calc = CalculatorProvider::new();
|
||||
|
||||
let result = calc.evaluate("= 5/2").unwrap();
|
||||
assert_eq!(result.name, "2.5");
|
||||
|
||||
let result = calc.evaluate("= 1/3").unwrap();
|
||||
assert!(result.name.starts_with("0.333"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_evaluate_functions() {
|
||||
let mut calc = CalculatorProvider::new();
|
||||
|
||||
let result = calc.evaluate("= sqrt(16)").unwrap();
|
||||
assert_eq!(result.name, "4");
|
||||
|
||||
let result = calc.evaluate("= abs(-5)").unwrap();
|
||||
assert_eq!(result.name, "5");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_evaluate_constants() {
|
||||
let mut calc = CalculatorProvider::new();
|
||||
|
||||
let result = calc.evaluate("= pi").unwrap();
|
||||
assert!(result.name.starts_with("3.14159"));
|
||||
|
||||
let result = calc.evaluate("= e").unwrap();
|
||||
assert!(result.name.starts_with("2.718"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_evaluate_invalid() {
|
||||
let mut calc = CalculatorProvider::new();
|
||||
|
||||
assert!(calc.evaluate("= ").is_none());
|
||||
assert!(calc.evaluate("= invalid").is_none());
|
||||
assert!(calc.evaluate("= 5 +").is_none());
|
||||
}
|
||||
}
|
||||
137
src/providers/clipboard.rs
Normal file
137
src/providers/clipboard.rs
Normal file
@@ -0,0 +1,137 @@
|
||||
use crate::providers::{LaunchItem, Provider, ProviderType};
|
||||
use log::{debug, warn};
|
||||
use std::process::Command;
|
||||
|
||||
/// Clipboard history provider - integrates with cliphist
|
||||
pub struct ClipboardProvider {
|
||||
items: Vec<LaunchItem>,
|
||||
max_entries: usize,
|
||||
}
|
||||
|
||||
impl ClipboardProvider {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
items: Vec::new(),
|
||||
max_entries: 50,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn with_max_entries(max_entries: usize) -> Self {
|
||||
Self {
|
||||
items: Vec::new(),
|
||||
max_entries,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if cliphist is available
|
||||
fn has_cliphist() -> bool {
|
||||
Command::new("which")
|
||||
.arg("cliphist")
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn load_clipboard_history(&mut self) {
|
||||
self.items.clear();
|
||||
|
||||
if !Self::has_cliphist() {
|
||||
debug!("cliphist not found, clipboard provider disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get clipboard history from cliphist
|
||||
let output = match Command::new("cliphist").arg("list").output() {
|
||||
Ok(o) => o,
|
||||
Err(e) => {
|
||||
warn!("Failed to run cliphist: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if !output.status.success() {
|
||||
debug!("cliphist list returned non-zero status");
|
||||
return;
|
||||
}
|
||||
|
||||
let content = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
for (idx, line) in content.lines().take(self.max_entries).enumerate() {
|
||||
// cliphist format: "id\tpreview"
|
||||
let parts: Vec<&str> = line.splitn(2, '\t').collect();
|
||||
|
||||
if parts.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let clip_id = parts[0];
|
||||
let preview = if parts.len() > 1 {
|
||||
// Truncate long previews
|
||||
let p = parts[1];
|
||||
if p.len() > 80 {
|
||||
format!("{}...", &p[..77])
|
||||
} else {
|
||||
p.to_string()
|
||||
}
|
||||
} else {
|
||||
"[binary data]".to_string()
|
||||
};
|
||||
|
||||
// Clean up preview - replace newlines with spaces
|
||||
let preview_clean = preview
|
||||
.replace('\n', " ")
|
||||
.replace('\r', "")
|
||||
.replace('\t', " ");
|
||||
|
||||
// Command to paste this entry
|
||||
// echo "id" | cliphist decode | wl-copy
|
||||
let command = format!(
|
||||
"echo '{}' | cliphist decode | wl-copy",
|
||||
clip_id.replace('\'', "'\\''")
|
||||
);
|
||||
|
||||
self.items.push(LaunchItem {
|
||||
id: format!("clipboard:{}", idx),
|
||||
name: preview_clean,
|
||||
description: Some("Copy to clipboard".to_string()),
|
||||
icon: Some("edit-paste".to_string()),
|
||||
provider: ProviderType::Clipboard,
|
||||
command,
|
||||
terminal: false,
|
||||
});
|
||||
}
|
||||
|
||||
debug!("Loaded {} clipboard entries", self.items.len());
|
||||
}
|
||||
}
|
||||
|
||||
impl Provider for ClipboardProvider {
|
||||
fn name(&self) -> &str {
|
||||
"Clipboard"
|
||||
}
|
||||
|
||||
fn provider_type(&self) -> ProviderType {
|
||||
ProviderType::Clipboard
|
||||
}
|
||||
|
||||
fn refresh(&mut self) {
|
||||
self.load_clipboard_history();
|
||||
}
|
||||
|
||||
fn items(&self) -> &[LaunchItem] {
|
||||
&self.items
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_clipboard_provider() {
|
||||
let mut provider = ClipboardProvider::new();
|
||||
provider.refresh();
|
||||
// Just ensure it doesn't panic - cliphist may not be installed
|
||||
}
|
||||
}
|
||||
@@ -17,17 +17,37 @@ impl DmenuProvider {
|
||||
}
|
||||
|
||||
/// Check if stdin has data (non-blocking check)
|
||||
/// Returns true only if stdin is a pipe or regular file with data available.
|
||||
/// Returns false for TTYs, /dev/null, and other character devices.
|
||||
pub fn has_stdin_data() -> bool {
|
||||
use std::os::unix::io::AsRawFd;
|
||||
|
||||
let stdin_fd = io::stdin().as_raw_fd();
|
||||
|
||||
// First check if stdin is a pipe or regular file (valid dmenu input sources)
|
||||
// Character devices (TTY, /dev/null) should NOT trigger dmenu mode
|
||||
let mut stat_buf: libc::stat = unsafe { std::mem::zeroed() };
|
||||
let stat_result = unsafe { libc::fstat(stdin_fd, &mut stat_buf) };
|
||||
if stat_result != 0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mode = stat_buf.st_mode;
|
||||
let is_pipe = (mode & libc::S_IFMT) == libc::S_IFIFO;
|
||||
let is_file = (mode & libc::S_IFMT) == libc::S_IFREG;
|
||||
|
||||
// Only check for data if stdin is a pipe or file
|
||||
if !is_pipe && !is_file {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Non-blocking poll to check if data is available
|
||||
let mut poll_fd = libc::pollfd {
|
||||
fd: stdin_fd,
|
||||
events: libc::POLLIN,
|
||||
revents: 0,
|
||||
};
|
||||
|
||||
// Non-blocking poll with 0 timeout
|
||||
let result = unsafe { libc::poll(&mut poll_fd, 1, 0) };
|
||||
result > 0 && (poll_fd.revents & libc::POLLIN) != 0
|
||||
}
|
||||
|
||||
446
src/providers/emoji.rs
Normal file
446
src/providers/emoji.rs
Normal file
@@ -0,0 +1,446 @@
|
||||
use crate::providers::{LaunchItem, Provider, ProviderType};
|
||||
|
||||
/// Emoji picker provider - search and copy emojis
|
||||
pub struct EmojiProvider {
|
||||
items: Vec<LaunchItem>,
|
||||
}
|
||||
|
||||
impl EmojiProvider {
|
||||
pub fn new() -> Self {
|
||||
Self { items: Vec::new() }
|
||||
}
|
||||
|
||||
fn load_emojis(&mut self) {
|
||||
self.items.clear();
|
||||
|
||||
// Common emojis with searchable names
|
||||
// Format: (emoji, name, keywords)
|
||||
let emojis: &[(&str, &str, &str)] = &[
|
||||
// Smileys & Emotion
|
||||
("😀", "grinning face", "smile happy"),
|
||||
("😃", "grinning face with big eyes", "smile happy"),
|
||||
("😄", "grinning face with smiling eyes", "smile happy laugh"),
|
||||
("😁", "beaming face with smiling eyes", "smile happy grin"),
|
||||
("😅", "grinning face with sweat", "smile nervous"),
|
||||
("🤣", "rolling on the floor laughing", "lol rofl funny"),
|
||||
("😂", "face with tears of joy", "laugh cry funny lol"),
|
||||
("🙂", "slightly smiling face", "smile"),
|
||||
("😊", "smiling face with smiling eyes", "blush happy"),
|
||||
("😇", "smiling face with halo", "angel innocent"),
|
||||
("🥰", "smiling face with hearts", "love adore"),
|
||||
("😍", "smiling face with heart-eyes", "love crush"),
|
||||
("🤩", "star-struck", "excited wow amazing"),
|
||||
("😘", "face blowing a kiss", "kiss love"),
|
||||
("😜", "winking face with tongue", "playful silly"),
|
||||
("🤪", "zany face", "crazy silly wild"),
|
||||
("😎", "smiling face with sunglasses", "cool"),
|
||||
("🤓", "nerd face", "geek glasses"),
|
||||
("🧐", "face with monocle", "thinking inspect"),
|
||||
("😏", "smirking face", "smug"),
|
||||
("😒", "unamused face", "meh annoyed"),
|
||||
("🙄", "face with rolling eyes", "whatever annoyed"),
|
||||
("😬", "grimacing face", "awkward nervous"),
|
||||
("😮💨", "face exhaling", "sigh relief"),
|
||||
("🤥", "lying face", "pinocchio lie"),
|
||||
("😌", "relieved face", "relaxed peaceful"),
|
||||
("😔", "pensive face", "sad thoughtful"),
|
||||
("😪", "sleepy face", "tired"),
|
||||
("🤤", "drooling face", "hungry yummy"),
|
||||
("😴", "sleeping face", "zzz tired"),
|
||||
("😷", "face with medical mask", "sick covid"),
|
||||
("🤒", "face with thermometer", "sick fever"),
|
||||
("🤕", "face with head-bandage", "hurt injured"),
|
||||
("🤢", "nauseated face", "sick gross"),
|
||||
("🤮", "face vomiting", "sick puke"),
|
||||
("🤧", "sneezing face", "achoo sick"),
|
||||
("🥵", "hot face", "sweating heat"),
|
||||
("🥶", "cold face", "freezing"),
|
||||
("😵", "face with crossed-out eyes", "dizzy dead"),
|
||||
("🤯", "exploding head", "mind blown wow"),
|
||||
("🤠", "cowboy hat face", "yeehaw western"),
|
||||
("🥳", "partying face", "celebration party"),
|
||||
("🥸", "disguised face", "incognito"),
|
||||
("😎", "cool face", "sunglasses"),
|
||||
("🤡", "clown face", "circus"),
|
||||
("👻", "ghost", "halloween spooky"),
|
||||
("💀", "skull", "dead death"),
|
||||
("☠️", "skull and crossbones", "danger death"),
|
||||
("👽", "alien", "ufo extraterrestrial"),
|
||||
("🤖", "robot", "bot android"),
|
||||
("💩", "pile of poo", "poop shit"),
|
||||
("😈", "smiling face with horns", "devil evil"),
|
||||
("👿", "angry face with horns", "devil evil"),
|
||||
// Gestures & People
|
||||
("👋", "waving hand", "hello hi bye wave"),
|
||||
("🤚", "raised back of hand", "stop"),
|
||||
("🖐️", "hand with fingers splayed", "five high"),
|
||||
("✋", "raised hand", "stop high five"),
|
||||
("🖖", "vulcan salute", "spock trek"),
|
||||
("👌", "ok hand", "okay perfect"),
|
||||
("🤌", "pinched fingers", "italian"),
|
||||
("🤏", "pinching hand", "small tiny"),
|
||||
("✌️", "victory hand", "peace two"),
|
||||
("🤞", "crossed fingers", "luck hope"),
|
||||
("🤟", "love-you gesture", "ily rock"),
|
||||
("🤘", "sign of the horns", "rock metal"),
|
||||
("🤙", "call me hand", "shaka hang loose"),
|
||||
("👈", "backhand index pointing left", "left point"),
|
||||
("👉", "backhand index pointing right", "right point"),
|
||||
("👆", "backhand index pointing up", "up point"),
|
||||
("👇", "backhand index pointing down", "down point"),
|
||||
("☝️", "index pointing up", "one point"),
|
||||
("👍", "thumbs up", "like yes good approve"),
|
||||
("👎", "thumbs down", "dislike no bad"),
|
||||
("✊", "raised fist", "power solidarity"),
|
||||
("👊", "oncoming fist", "punch bump"),
|
||||
("🤛", "left-facing fist", "fist bump"),
|
||||
("🤜", "right-facing fist", "fist bump"),
|
||||
("👏", "clapping hands", "applause bravo"),
|
||||
("🙌", "raising hands", "hooray celebrate"),
|
||||
("👐", "open hands", "hug"),
|
||||
("🤲", "palms up together", "prayer"),
|
||||
("🤝", "handshake", "agreement deal"),
|
||||
("🙏", "folded hands", "prayer please thanks"),
|
||||
("✍️", "writing hand", "write"),
|
||||
("💪", "flexed biceps", "strong muscle"),
|
||||
("🦾", "mechanical arm", "robot prosthetic"),
|
||||
("🦵", "leg", "kick"),
|
||||
("🦶", "foot", "kick"),
|
||||
("👂", "ear", "listen hear"),
|
||||
("👃", "nose", "smell"),
|
||||
("🧠", "brain", "smart think"),
|
||||
("👀", "eyes", "look see watch"),
|
||||
("👁️", "eye", "see look"),
|
||||
("👅", "tongue", "taste lick"),
|
||||
("👄", "mouth", "lips kiss"),
|
||||
// Hearts & Love
|
||||
("❤️", "red heart", "love"),
|
||||
("🧡", "orange heart", "love"),
|
||||
("💛", "yellow heart", "love friendship"),
|
||||
("💚", "green heart", "love"),
|
||||
("💙", "blue heart", "love"),
|
||||
("💜", "purple heart", "love"),
|
||||
("🖤", "black heart", "love dark"),
|
||||
("🤍", "white heart", "love pure"),
|
||||
("🤎", "brown heart", "love"),
|
||||
("💔", "broken heart", "heartbreak sad"),
|
||||
("❤️🔥", "heart on fire", "passion love"),
|
||||
("❤️🩹", "mending heart", "healing recovery"),
|
||||
("💕", "two hearts", "love"),
|
||||
("💞", "revolving hearts", "love"),
|
||||
("💓", "beating heart", "love"),
|
||||
("💗", "growing heart", "love"),
|
||||
("💖", "sparkling heart", "love"),
|
||||
("💘", "heart with arrow", "love cupid"),
|
||||
("💝", "heart with ribbon", "love gift"),
|
||||
("💟", "heart decoration", "love"),
|
||||
// Animals
|
||||
("🐶", "dog face", "puppy"),
|
||||
("🐱", "cat face", "kitty"),
|
||||
("🐭", "mouse face", ""),
|
||||
("🐹", "hamster", ""),
|
||||
("🐰", "rabbit face", "bunny"),
|
||||
("🦊", "fox", ""),
|
||||
("🐻", "bear", ""),
|
||||
("🐼", "panda", ""),
|
||||
("🐨", "koala", ""),
|
||||
("🐯", "tiger face", ""),
|
||||
("🦁", "lion", ""),
|
||||
("🐮", "cow face", ""),
|
||||
("🐷", "pig face", ""),
|
||||
("🐸", "frog", ""),
|
||||
("🐵", "monkey face", ""),
|
||||
("🦄", "unicorn", "magic"),
|
||||
("🐝", "bee", "honeybee"),
|
||||
("🦋", "butterfly", ""),
|
||||
("🐌", "snail", "slow"),
|
||||
("🐛", "bug", "caterpillar"),
|
||||
("🦀", "crab", ""),
|
||||
("🐙", "octopus", ""),
|
||||
("🐠", "tropical fish", ""),
|
||||
("🐟", "fish", ""),
|
||||
("🐬", "dolphin", ""),
|
||||
("🐳", "whale", ""),
|
||||
("🦈", "shark", ""),
|
||||
("🐊", "crocodile", "alligator"),
|
||||
("🐢", "turtle", ""),
|
||||
("🦎", "lizard", ""),
|
||||
("🐍", "snake", ""),
|
||||
("🦖", "t-rex", "dinosaur"),
|
||||
("🦕", "sauropod", "dinosaur"),
|
||||
("🐔", "chicken", ""),
|
||||
("🐧", "penguin", ""),
|
||||
("🦅", "eagle", "bird"),
|
||||
("🦆", "duck", ""),
|
||||
("🦉", "owl", ""),
|
||||
// Food & Drink
|
||||
("🍎", "red apple", "fruit"),
|
||||
("🍐", "pear", "fruit"),
|
||||
("🍊", "orange", "tangerine fruit"),
|
||||
("🍋", "lemon", "fruit"),
|
||||
("🍌", "banana", "fruit"),
|
||||
("🍉", "watermelon", "fruit"),
|
||||
("🍇", "grapes", "fruit"),
|
||||
("🍓", "strawberry", "fruit"),
|
||||
("🍒", "cherries", "fruit"),
|
||||
("🍑", "peach", "fruit"),
|
||||
("🥭", "mango", "fruit"),
|
||||
("🍍", "pineapple", "fruit"),
|
||||
("🥥", "coconut", "fruit"),
|
||||
("🥝", "kiwi", "fruit"),
|
||||
("🍅", "tomato", "vegetable"),
|
||||
("🥑", "avocado", ""),
|
||||
("🥦", "broccoli", "vegetable"),
|
||||
("🥬", "leafy green", "vegetable salad"),
|
||||
("🥒", "cucumber", "vegetable"),
|
||||
("🌶️", "hot pepper", "spicy chili"),
|
||||
("🌽", "corn", ""),
|
||||
("🥕", "carrot", "vegetable"),
|
||||
("🧄", "garlic", ""),
|
||||
("🧅", "onion", ""),
|
||||
("🥔", "potato", ""),
|
||||
("🍞", "bread", ""),
|
||||
("🥐", "croissant", ""),
|
||||
("🥖", "baguette", "bread french"),
|
||||
("🥨", "pretzel", ""),
|
||||
("🧀", "cheese", ""),
|
||||
("🥚", "egg", ""),
|
||||
("🍳", "cooking", "frying pan egg"),
|
||||
("🥞", "pancakes", "breakfast"),
|
||||
("🧇", "waffle", "breakfast"),
|
||||
("🥓", "bacon", "breakfast"),
|
||||
("🍔", "hamburger", "burger"),
|
||||
("🍟", "french fries", ""),
|
||||
("🍕", "pizza", ""),
|
||||
("🌭", "hot dog", ""),
|
||||
("🥪", "sandwich", ""),
|
||||
("🌮", "taco", "mexican"),
|
||||
("🌯", "burrito", "mexican"),
|
||||
("🍜", "steaming bowl", "ramen noodles"),
|
||||
("🍝", "spaghetti", "pasta"),
|
||||
("🍣", "sushi", "japanese"),
|
||||
("🍱", "bento box", "japanese"),
|
||||
("🍩", "doughnut", "donut dessert"),
|
||||
("🍪", "cookie", "dessert"),
|
||||
("🎂", "birthday cake", "dessert"),
|
||||
("🍰", "shortcake", "dessert"),
|
||||
("🧁", "cupcake", "dessert"),
|
||||
("🍫", "chocolate bar", "dessert"),
|
||||
("🍬", "candy", "sweet"),
|
||||
("🍭", "lollipop", "candy sweet"),
|
||||
("🍦", "soft ice cream", "dessert"),
|
||||
("🍨", "ice cream", "dessert"),
|
||||
("☕", "hot beverage", "coffee tea"),
|
||||
("🍵", "teacup", "tea"),
|
||||
("🧃", "juice box", ""),
|
||||
("🥤", "cup with straw", "soda drink"),
|
||||
("🍺", "beer mug", "drink alcohol"),
|
||||
("🍻", "clinking beer mugs", "cheers drink"),
|
||||
("🥂", "clinking glasses", "champagne cheers"),
|
||||
("🍷", "wine glass", "drink alcohol"),
|
||||
("🥃", "tumbler glass", "whiskey drink"),
|
||||
("🍸", "cocktail glass", "martini drink"),
|
||||
// Objects & Symbols
|
||||
("💻", "laptop", "computer"),
|
||||
("🖥️", "desktop computer", "pc"),
|
||||
("⌨️", "keyboard", ""),
|
||||
("🖱️", "computer mouse", ""),
|
||||
("💾", "floppy disk", "save"),
|
||||
("💿", "optical disk", "cd"),
|
||||
("📱", "mobile phone", "smartphone"),
|
||||
("☎️", "telephone", "phone"),
|
||||
("📧", "email", "mail"),
|
||||
("📨", "incoming envelope", "email"),
|
||||
("📩", "envelope with arrow", "email send"),
|
||||
("📝", "memo", "note write"),
|
||||
("📄", "page facing up", "document"),
|
||||
("📃", "page with curl", "document"),
|
||||
("📑", "bookmark tabs", ""),
|
||||
("📚", "books", "library read"),
|
||||
("📖", "open book", "read"),
|
||||
("🔗", "link", "chain url"),
|
||||
("📎", "paperclip", "attachment"),
|
||||
("🔒", "locked", "security"),
|
||||
("🔓", "unlocked", "security open"),
|
||||
("🔑", "key", "password"),
|
||||
("🔧", "wrench", "tool fix"),
|
||||
("🔨", "hammer", "tool"),
|
||||
("⚙️", "gear", "settings"),
|
||||
("🧲", "magnet", ""),
|
||||
("💡", "light bulb", "idea"),
|
||||
("🔦", "flashlight", ""),
|
||||
("🔋", "battery", "power"),
|
||||
("🔌", "electric plug", "power"),
|
||||
("💰", "money bag", ""),
|
||||
("💵", "dollar", "money cash"),
|
||||
("💳", "credit card", "payment"),
|
||||
("⏰", "alarm clock", "time"),
|
||||
("⏱️", "stopwatch", "timer"),
|
||||
("📅", "calendar", "date"),
|
||||
("📆", "tear-off calendar", "date"),
|
||||
("✅", "check mark", "done yes"),
|
||||
("❌", "cross mark", "no wrong delete"),
|
||||
("❓", "question mark", "help"),
|
||||
("❗", "exclamation mark", "important warning"),
|
||||
("⚠️", "warning", "caution alert"),
|
||||
("🚫", "prohibited", "no ban forbidden"),
|
||||
("⭕", "hollow circle", ""),
|
||||
("🔴", "red circle", ""),
|
||||
("🟠", "orange circle", ""),
|
||||
("🟡", "yellow circle", ""),
|
||||
("🟢", "green circle", ""),
|
||||
("🔵", "blue circle", ""),
|
||||
("🟣", "purple circle", ""),
|
||||
("⚫", "black circle", ""),
|
||||
("⚪", "white circle", ""),
|
||||
("🟤", "brown circle", ""),
|
||||
("⬛", "black square", ""),
|
||||
("⬜", "white square", ""),
|
||||
("🔶", "large orange diamond", ""),
|
||||
("🔷", "large blue diamond", ""),
|
||||
("⭐", "star", "favorite"),
|
||||
("🌟", "glowing star", "sparkle"),
|
||||
("✨", "sparkles", "magic shine"),
|
||||
("💫", "dizzy", "star"),
|
||||
("🔥", "fire", "hot lit"),
|
||||
("💧", "droplet", "water"),
|
||||
("🌊", "wave", "water ocean"),
|
||||
("🎵", "musical note", "music"),
|
||||
("🎶", "musical notes", "music"),
|
||||
("🎤", "microphone", "sing karaoke"),
|
||||
("🎧", "headphones", "music"),
|
||||
("🎮", "video game", "gaming controller"),
|
||||
("🕹️", "joystick", "gaming"),
|
||||
("🎯", "direct hit", "target bullseye"),
|
||||
("🏆", "trophy", "winner award"),
|
||||
("🥇", "1st place medal", "gold winner"),
|
||||
("🥈", "2nd place medal", "silver"),
|
||||
("🥉", "3rd place medal", "bronze"),
|
||||
("🎁", "wrapped gift", "present"),
|
||||
("🎈", "balloon", "party"),
|
||||
("🎉", "party popper", "celebration tada"),
|
||||
("🎊", "confetti ball", "celebration"),
|
||||
// Arrows & Misc
|
||||
("➡️", "right arrow", ""),
|
||||
("⬅️", "left arrow", ""),
|
||||
("⬆️", "up arrow", ""),
|
||||
("⬇️", "down arrow", ""),
|
||||
("↗️", "up-right arrow", ""),
|
||||
("↘️", "down-right arrow", ""),
|
||||
("↙️", "down-left arrow", ""),
|
||||
("↖️", "up-left arrow", ""),
|
||||
("↕️", "up-down arrow", ""),
|
||||
("↔️", "left-right arrow", ""),
|
||||
("🔄", "counterclockwise arrows", "refresh reload"),
|
||||
("🔃", "clockwise arrows", "refresh reload"),
|
||||
("➕", "plus", "add"),
|
||||
("➖", "minus", "subtract"),
|
||||
("➗", "division", "divide"),
|
||||
("✖️", "multiply", "times"),
|
||||
("♾️", "infinity", "forever"),
|
||||
("💯", "hundred points", "100 perfect"),
|
||||
("🆗", "ok button", "okay"),
|
||||
("🆕", "new button", ""),
|
||||
("🆓", "free button", ""),
|
||||
("ℹ️", "information", "info"),
|
||||
("🅿️", "parking", ""),
|
||||
("🚀", "rocket", "launch startup"),
|
||||
("✈️", "airplane", "travel flight"),
|
||||
("🚗", "car", "automobile"),
|
||||
("🚕", "taxi", "cab"),
|
||||
("🚌", "bus", ""),
|
||||
("🚂", "locomotive", "train"),
|
||||
("🏠", "house", "home"),
|
||||
("🏢", "office building", "work"),
|
||||
("🏥", "hospital", ""),
|
||||
("🏫", "school", ""),
|
||||
("🏛️", "classical building", ""),
|
||||
("⛪", "church", ""),
|
||||
("🕌", "mosque", ""),
|
||||
("🕍", "synagogue", ""),
|
||||
("🗽", "statue of liberty", "usa america"),
|
||||
("🗼", "tokyo tower", "japan"),
|
||||
("🗾", "map of japan", ""),
|
||||
("🌍", "globe europe-africa", "earth world"),
|
||||
("🌎", "globe americas", "earth world"),
|
||||
("🌏", "globe asia-australia", "earth world"),
|
||||
("🌑", "new moon", ""),
|
||||
("🌕", "full moon", ""),
|
||||
("☀️", "sun", "sunny"),
|
||||
("🌙", "crescent moon", "night"),
|
||||
("⭐", "star", ""),
|
||||
("☁️", "cloud", ""),
|
||||
("🌧️", "cloud with rain", "rainy"),
|
||||
("⛈️", "cloud with lightning", "storm thunder"),
|
||||
("🌈", "rainbow", ""),
|
||||
("❄️", "snowflake", "cold winter"),
|
||||
("☃️", "snowman", "winter"),
|
||||
("🎄", "christmas tree", "xmas holiday"),
|
||||
("🎃", "jack-o-lantern", "halloween pumpkin"),
|
||||
("🐚", "shell", "beach"),
|
||||
("🌸", "cherry blossom", "flower spring"),
|
||||
("🌺", "hibiscus", "flower"),
|
||||
("🌻", "sunflower", "flower"),
|
||||
("🌹", "rose", "flower love"),
|
||||
("🌷", "tulip", "flower"),
|
||||
("🌱", "seedling", "plant grow"),
|
||||
("🌲", "evergreen tree", ""),
|
||||
("🌳", "deciduous tree", ""),
|
||||
("🌴", "palm tree", "tropical"),
|
||||
("🌵", "cactus", "desert"),
|
||||
("🍀", "four leaf clover", "luck irish"),
|
||||
("🍁", "maple leaf", "fall autumn canada"),
|
||||
("🍂", "fallen leaf", "fall autumn"),
|
||||
];
|
||||
|
||||
for (emoji, name, keywords) in emojis {
|
||||
// Combine name and keywords for better searching
|
||||
let search_text = format!("{} {}", name, keywords);
|
||||
|
||||
self.items.push(LaunchItem {
|
||||
id: format!("emoji:{}", emoji),
|
||||
name: name.to_string(),
|
||||
description: Some(format!("{} {}", emoji, keywords)),
|
||||
icon: None,
|
||||
provider: ProviderType::Emoji,
|
||||
// Copy emoji to clipboard using wl-copy
|
||||
command: format!("printf '%s' '{}' | wl-copy", emoji),
|
||||
terminal: false,
|
||||
});
|
||||
|
||||
// Store the search text for matching (not used directly but could be)
|
||||
let _ = search_text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Provider for EmojiProvider {
|
||||
fn name(&self) -> &str {
|
||||
"Emoji"
|
||||
}
|
||||
|
||||
fn provider_type(&self) -> ProviderType {
|
||||
ProviderType::Emoji
|
||||
}
|
||||
|
||||
fn refresh(&mut self) {
|
||||
self.load_emojis();
|
||||
}
|
||||
|
||||
fn items(&self) -> &[LaunchItem] {
|
||||
&self.items
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_emoji_provider() {
|
||||
let mut provider = EmojiProvider::new();
|
||||
provider.refresh();
|
||||
assert!(provider.items().len() > 100);
|
||||
assert!(provider.items().iter().any(|i| i.name.contains("😀")));
|
||||
}
|
||||
}
|
||||
224
src/providers/files.rs
Normal file
224
src/providers/files.rs
Normal file
@@ -0,0 +1,224 @@
|
||||
use crate::providers::{LaunchItem, ProviderType};
|
||||
use log::{debug, warn};
|
||||
use std::process::Command;
|
||||
|
||||
/// File search provider - uses fd or locate for fast file finding
|
||||
pub struct FileSearchProvider {
|
||||
search_tool: SearchTool,
|
||||
max_results: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum SearchTool {
|
||||
Fd,
|
||||
Locate,
|
||||
None,
|
||||
}
|
||||
|
||||
impl FileSearchProvider {
|
||||
pub fn new() -> Self {
|
||||
let search_tool = Self::detect_search_tool();
|
||||
debug!("File search using: {:?}", search_tool);
|
||||
|
||||
Self {
|
||||
search_tool,
|
||||
max_results: 20,
|
||||
}
|
||||
}
|
||||
|
||||
fn detect_search_tool() -> SearchTool {
|
||||
// Prefer fd (faster, respects .gitignore)
|
||||
if Self::command_exists("fd") {
|
||||
return SearchTool::Fd;
|
||||
}
|
||||
// Fall back to locate (requires updatedb)
|
||||
if Self::command_exists("locate") {
|
||||
return SearchTool::Locate;
|
||||
}
|
||||
SearchTool::None
|
||||
}
|
||||
|
||||
fn command_exists(cmd: &str) -> bool {
|
||||
Command::new("which")
|
||||
.arg(cmd)
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Check if query is a file search query
|
||||
/// Triggers on: `/ query`, `file query`, `find query`
|
||||
pub fn is_file_query(query: &str) -> bool {
|
||||
let trimmed = query.trim();
|
||||
trimmed.starts_with("/ ")
|
||||
|| trimmed.starts_with("/")
|
||||
|| trimmed.to_lowercase().starts_with("file ")
|
||||
|| trimmed.to_lowercase().starts_with("find ")
|
||||
}
|
||||
|
||||
/// Extract the search term from the query
|
||||
fn extract_search_term(query: &str) -> Option<&str> {
|
||||
let trimmed = query.trim();
|
||||
|
||||
if let Some(rest) = trimmed.strip_prefix("/ ") {
|
||||
Some(rest.trim())
|
||||
} else if let Some(rest) = trimmed.strip_prefix("/") {
|
||||
Some(rest.trim())
|
||||
} else if trimmed.to_lowercase().starts_with("file ") {
|
||||
Some(trimmed[5..].trim())
|
||||
} else if trimmed.to_lowercase().starts_with("find ") {
|
||||
Some(trimmed[5..].trim())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluate a file search query
|
||||
pub fn evaluate(&self, query: &str) -> Vec<LaunchItem> {
|
||||
let search_term = match Self::extract_search_term(query) {
|
||||
Some(t) if !t.is_empty() => t,
|
||||
_ => return Vec::new(),
|
||||
};
|
||||
|
||||
self.search_files(search_term)
|
||||
}
|
||||
|
||||
/// Evaluate a raw search term (for :file filter mode)
|
||||
pub fn evaluate_raw(&self, search_term: &str) -> Vec<LaunchItem> {
|
||||
let trimmed = search_term.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
self.search_files(trimmed)
|
||||
}
|
||||
|
||||
fn search_files(&self, pattern: &str) -> Vec<LaunchItem> {
|
||||
match self.search_tool {
|
||||
SearchTool::Fd => self.search_with_fd(pattern),
|
||||
SearchTool::Locate => self.search_with_locate(pattern),
|
||||
SearchTool::None => {
|
||||
debug!("No file search tool available");
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn search_with_fd(&self, pattern: &str) -> Vec<LaunchItem> {
|
||||
// fd searches from home directory by default
|
||||
let home = dirs::home_dir().unwrap_or_default();
|
||||
|
||||
let output = match Command::new("fd")
|
||||
.args([
|
||||
"--max-results",
|
||||
&self.max_results.to_string(),
|
||||
"--type",
|
||||
"f", // Files only
|
||||
"--type",
|
||||
"d", // And directories
|
||||
pattern,
|
||||
])
|
||||
.current_dir(&home)
|
||||
.output()
|
||||
{
|
||||
Ok(o) => o,
|
||||
Err(e) => {
|
||||
warn!("Failed to run fd: {}", e);
|
||||
return Vec::new();
|
||||
}
|
||||
};
|
||||
|
||||
self.parse_file_results(&String::from_utf8_lossy(&output.stdout), &home)
|
||||
}
|
||||
|
||||
fn search_with_locate(&self, pattern: &str) -> Vec<LaunchItem> {
|
||||
let home = dirs::home_dir().unwrap_or_default();
|
||||
|
||||
let output = match Command::new("locate")
|
||||
.args([
|
||||
"--limit",
|
||||
&self.max_results.to_string(),
|
||||
"--ignore-case",
|
||||
pattern,
|
||||
])
|
||||
.output()
|
||||
{
|
||||
Ok(o) => o,
|
||||
Err(e) => {
|
||||
warn!("Failed to run locate: {}", e);
|
||||
return Vec::new();
|
||||
}
|
||||
};
|
||||
|
||||
self.parse_file_results(&String::from_utf8_lossy(&output.stdout), &home)
|
||||
}
|
||||
|
||||
fn parse_file_results(&self, output: &str, home: &std::path::Path) -> Vec<LaunchItem> {
|
||||
output
|
||||
.lines()
|
||||
.filter(|line| !line.is_empty())
|
||||
.map(|path| {
|
||||
let path = path.trim();
|
||||
let full_path = if path.starts_with('/') {
|
||||
path.to_string()
|
||||
} else {
|
||||
home.join(path).to_string_lossy().to_string()
|
||||
};
|
||||
|
||||
// Get filename for display
|
||||
let filename = std::path::Path::new(&full_path)
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| full_path.clone());
|
||||
|
||||
// Determine icon based on whether it's a directory
|
||||
let is_dir = std::path::Path::new(&full_path).is_dir();
|
||||
let icon = if is_dir {
|
||||
"folder"
|
||||
} else {
|
||||
"text-x-generic"
|
||||
};
|
||||
|
||||
// Command to open with xdg-open
|
||||
let command = format!("xdg-open '{}'", full_path.replace('\'', "'\\''"));
|
||||
|
||||
LaunchItem {
|
||||
id: format!("file:{}", full_path),
|
||||
name: filename,
|
||||
description: Some(full_path.clone()),
|
||||
icon: Some(icon.to_string()),
|
||||
provider: ProviderType::Files,
|
||||
command,
|
||||
terminal: false,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_is_file_query() {
|
||||
assert!(FileSearchProvider::is_file_query("/ config"));
|
||||
assert!(FileSearchProvider::is_file_query("/config"));
|
||||
assert!(FileSearchProvider::is_file_query("file config"));
|
||||
assert!(FileSearchProvider::is_file_query("find config"));
|
||||
assert!(!FileSearchProvider::is_file_query("config"));
|
||||
assert!(!FileSearchProvider::is_file_query("? search"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_search_term() {
|
||||
assert_eq!(
|
||||
FileSearchProvider::extract_search_term("/ config.toml"),
|
||||
Some("config.toml")
|
||||
);
|
||||
assert_eq!(
|
||||
FileSearchProvider::extract_search_term("file bashrc"),
|
||||
Some("bashrc")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,37 @@
|
||||
mod application;
|
||||
mod bookmarks;
|
||||
mod calculator;
|
||||
mod clipboard;
|
||||
mod command;
|
||||
mod dmenu;
|
||||
mod emoji;
|
||||
mod files;
|
||||
mod scripts;
|
||||
mod ssh;
|
||||
mod system;
|
||||
mod uuctl;
|
||||
mod websearch;
|
||||
|
||||
pub use application::ApplicationProvider;
|
||||
pub use bookmarks::BookmarksProvider;
|
||||
pub use calculator::CalculatorProvider;
|
||||
pub use clipboard::ClipboardProvider;
|
||||
pub use command::CommandProvider;
|
||||
pub use dmenu::DmenuProvider;
|
||||
pub use emoji::EmojiProvider;
|
||||
pub use files::FileSearchProvider;
|
||||
pub use scripts::ScriptsProvider;
|
||||
pub use ssh::SshProvider;
|
||||
pub use system::SystemProvider;
|
||||
pub use uuctl::UuctlProvider;
|
||||
pub use websearch::WebSearchProvider;
|
||||
|
||||
use fuzzy_matcher::FuzzyMatcher;
|
||||
use fuzzy_matcher::skim::SkimMatcherV2;
|
||||
use log::info;
|
||||
|
||||
use crate::data::FrecencyStore;
|
||||
|
||||
/// Represents a single searchable/launchable item
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LaunchItem {
|
||||
@@ -28,9 +48,18 @@ pub struct LaunchItem {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum ProviderType {
|
||||
Application,
|
||||
Bookmarks,
|
||||
Calculator,
|
||||
Clipboard,
|
||||
Command,
|
||||
Dmenu,
|
||||
Emoji,
|
||||
Files,
|
||||
Scripts,
|
||||
Ssh,
|
||||
System,
|
||||
Uuctl,
|
||||
WebSearch,
|
||||
}
|
||||
|
||||
impl std::str::FromStr for ProviderType {
|
||||
@@ -39,10 +68,22 @@ impl std::str::FromStr for ProviderType {
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"app" | "apps" | "application" | "applications" => Ok(ProviderType::Application),
|
||||
"bookmark" | "bookmarks" | "bm" => Ok(ProviderType::Bookmarks),
|
||||
"calc" | "calculator" => Ok(ProviderType::Calculator),
|
||||
"clip" | "clipboard" => Ok(ProviderType::Clipboard),
|
||||
"cmd" | "command" | "commands" => Ok(ProviderType::Command),
|
||||
"uuctl" => Ok(ProviderType::Uuctl),
|
||||
"dmenu" => Ok(ProviderType::Dmenu),
|
||||
_ => Err(format!("Unknown provider: '{}'. Valid: app, cmd, uuctl", s)),
|
||||
"emoji" | "emojis" => Ok(ProviderType::Emoji),
|
||||
"file" | "files" | "find" => Ok(ProviderType::Files),
|
||||
"script" | "scripts" => Ok(ProviderType::Scripts),
|
||||
"ssh" => Ok(ProviderType::Ssh),
|
||||
"sys" | "system" | "power" => Ok(ProviderType::System),
|
||||
"uuctl" => Ok(ProviderType::Uuctl),
|
||||
"web" | "websearch" | "search" => Ok(ProviderType::WebSearch),
|
||||
_ => Err(format!(
|
||||
"Unknown provider: '{}'. Valid: app, bookmark, calc, clip, cmd, emoji, file, script, ssh, sys, web",
|
||||
s
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,9 +92,18 @@ impl std::fmt::Display for ProviderType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ProviderType::Application => write!(f, "app"),
|
||||
ProviderType::Bookmarks => write!(f, "bookmark"),
|
||||
ProviderType::Calculator => write!(f, "calc"),
|
||||
ProviderType::Clipboard => write!(f, "clip"),
|
||||
ProviderType::Command => write!(f, "cmd"),
|
||||
ProviderType::Dmenu => write!(f, "dmenu"),
|
||||
ProviderType::Emoji => write!(f, "emoji"),
|
||||
ProviderType::Files => write!(f, "file"),
|
||||
ProviderType::Scripts => write!(f, "script"),
|
||||
ProviderType::Ssh => write!(f, "ssh"),
|
||||
ProviderType::System => write!(f, "sys"),
|
||||
ProviderType::Uuctl => write!(f, "uuctl"),
|
||||
ProviderType::WebSearch => write!(f, "web"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,13 +120,28 @@ pub trait Provider: Send {
|
||||
/// Manages all providers and handles searching
|
||||
pub struct ProviderManager {
|
||||
providers: Vec<Box<dyn Provider>>,
|
||||
calculator: CalculatorProvider,
|
||||
websearch: WebSearchProvider,
|
||||
filesearch: FileSearchProvider,
|
||||
matcher: SkimMatcherV2,
|
||||
}
|
||||
|
||||
impl ProviderManager {
|
||||
#[allow(dead_code)]
|
||||
pub fn new() -> Self {
|
||||
Self::with_search_engine("duckduckgo")
|
||||
}
|
||||
|
||||
pub fn with_search_engine(search_engine: &str) -> Self {
|
||||
Self::with_config(search_engine, "kitty")
|
||||
}
|
||||
|
||||
pub fn with_config(search_engine: &str, terminal: &str) -> Self {
|
||||
let mut manager = Self {
|
||||
providers: Vec::new(),
|
||||
calculator: CalculatorProvider::new(),
|
||||
websearch: WebSearchProvider::with_engine(search_engine),
|
||||
filesearch: FileSearchProvider::new(),
|
||||
matcher: SkimMatcherV2::default(),
|
||||
};
|
||||
|
||||
@@ -93,6 +158,14 @@ impl ProviderManager {
|
||||
manager.providers.push(Box::new(ApplicationProvider::new()));
|
||||
manager.providers.push(Box::new(CommandProvider::new()));
|
||||
manager.providers.push(Box::new(UuctlProvider::new()));
|
||||
|
||||
// New providers
|
||||
manager.providers.push(Box::new(SystemProvider::new()));
|
||||
manager.providers.push(Box::new(SshProvider::with_terminal(terminal)));
|
||||
manager.providers.push(Box::new(ClipboardProvider::new()));
|
||||
manager.providers.push(Box::new(BookmarksProvider::new()));
|
||||
manager.providers.push(Box::new(EmojiProvider::new()));
|
||||
manager.providers.push(Box::new(ScriptsProvider::new()));
|
||||
}
|
||||
|
||||
// Initial refresh
|
||||
@@ -206,6 +279,117 @@ impl ProviderManager {
|
||||
results
|
||||
}
|
||||
|
||||
/// Search with frecency boosting and calculator support
|
||||
pub fn search_with_frecency(
|
||||
&mut self,
|
||||
query: &str,
|
||||
max_results: usize,
|
||||
filter: &crate::filter::ProviderFilter,
|
||||
frecency: &FrecencyStore,
|
||||
frecency_weight: f64,
|
||||
) -> Vec<(LaunchItem, i64)> {
|
||||
let mut results: Vec<(LaunchItem, i64)> = Vec::new();
|
||||
|
||||
// Check for calculator query (= or calc prefix)
|
||||
if CalculatorProvider::is_calculator_query(query) {
|
||||
if let Some(calc_result) = self.calculator.evaluate(query) {
|
||||
// Calculator results get a high score to appear first
|
||||
results.push((calc_result, 10000));
|
||||
}
|
||||
}
|
||||
// Also check for raw expression when in :calc filter mode
|
||||
else if filter.active_prefix() == Some(ProviderType::Calculator)
|
||||
&& CalculatorProvider::looks_like_expression(query)
|
||||
{
|
||||
if let Some(calc_result) = self.calculator.evaluate_raw(query) {
|
||||
results.push((calc_result, 10000));
|
||||
}
|
||||
}
|
||||
|
||||
// Check for web search query
|
||||
if WebSearchProvider::is_websearch_query(query) {
|
||||
if let Some(web_result) = self.websearch.evaluate(query) {
|
||||
// Web search results get a high score to appear first
|
||||
results.push((web_result, 9000));
|
||||
}
|
||||
}
|
||||
// Also check for raw query when in :web filter mode
|
||||
else if filter.active_prefix() == Some(ProviderType::WebSearch) && !query.is_empty() {
|
||||
if let Some(web_result) = self.websearch.evaluate_raw(query) {
|
||||
results.push((web_result, 9000));
|
||||
}
|
||||
}
|
||||
|
||||
// Check for file search query
|
||||
if FileSearchProvider::is_file_query(query) {
|
||||
let file_results = self.filesearch.evaluate(query);
|
||||
for (idx, item) in file_results.into_iter().enumerate() {
|
||||
// Score decreases for each result to maintain order
|
||||
results.push((item, 8000 - idx as i64));
|
||||
}
|
||||
}
|
||||
// Also check for raw query when in :file filter mode
|
||||
else if filter.active_prefix() == Some(ProviderType::Files) && !query.is_empty() {
|
||||
let file_results = self.filesearch.evaluate_raw(query);
|
||||
for (idx, item) in file_results.into_iter().enumerate() {
|
||||
results.push((item, 8000 - idx as i64));
|
||||
}
|
||||
}
|
||||
|
||||
// Empty query (after checking special providers) - return frecency-sorted items
|
||||
if query.is_empty() {
|
||||
let mut items: Vec<(LaunchItem, i64)> = self
|
||||
.providers
|
||||
.iter()
|
||||
.filter(|p| filter.is_active(p.provider_type()))
|
||||
.flat_map(|p| p.items().iter().cloned())
|
||||
.map(|item| {
|
||||
let frecency_score = frecency.get_score(&item.id);
|
||||
let boosted = (frecency_score * frecency_weight * 100.0) as i64;
|
||||
(item, boosted)
|
||||
})
|
||||
.collect();
|
||||
|
||||
items.sort_by(|a, b| b.1.cmp(&a.1));
|
||||
items.truncate(max_results);
|
||||
return items;
|
||||
}
|
||||
|
||||
// Regular search with frecency boost
|
||||
let search_results: Vec<(LaunchItem, i64)> = self
|
||||
.providers
|
||||
.iter()
|
||||
.filter(|provider| filter.is_active(provider.provider_type()))
|
||||
.flat_map(|provider| {
|
||||
provider.items().iter().filter_map(|item| {
|
||||
let name_score = self.matcher.fuzzy_match(&item.name, query);
|
||||
let desc_score = item
|
||||
.description
|
||||
.as_ref()
|
||||
.and_then(|d| self.matcher.fuzzy_match(d, query));
|
||||
|
||||
let base_score = match (name_score, desc_score) {
|
||||
(Some(n), Some(d)) => Some(n.max(d)),
|
||||
(Some(n), None) => Some(n),
|
||||
(None, Some(d)) => Some(d / 2),
|
||||
(None, None) => None,
|
||||
};
|
||||
|
||||
base_score.map(|s| {
|
||||
let frecency_score = frecency.get_score(&item.id);
|
||||
let frecency_boost = (frecency_score * frecency_weight * 10.0) as i64;
|
||||
(item.clone(), s + frecency_boost)
|
||||
})
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
results.extend(search_results);
|
||||
results.sort_by(|a, b| b.1.cmp(&a.1));
|
||||
results.truncate(max_results);
|
||||
results
|
||||
}
|
||||
|
||||
/// Get all available provider types (for UI tabs)
|
||||
#[allow(dead_code)]
|
||||
pub fn available_providers(&self) -> Vec<ProviderType> {
|
||||
|
||||
181
src/providers/scripts.rs
Normal file
181
src/providers/scripts.rs
Normal file
@@ -0,0 +1,181 @@
|
||||
use crate::providers::{LaunchItem, Provider, ProviderType};
|
||||
use log::{debug, warn};
|
||||
use std::fs;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Custom scripts provider - runs user scripts from ~/.config/owlry/scripts/
|
||||
pub struct ScriptsProvider {
|
||||
items: Vec<LaunchItem>,
|
||||
}
|
||||
|
||||
impl ScriptsProvider {
|
||||
pub fn new() -> Self {
|
||||
Self { items: Vec::new() }
|
||||
}
|
||||
|
||||
fn scripts_dir() -> Option<PathBuf> {
|
||||
dirs::config_dir().map(|p| p.join("owlry").join("scripts"))
|
||||
}
|
||||
|
||||
fn load_scripts(&mut self) {
|
||||
self.items.clear();
|
||||
|
||||
let scripts_dir = match Self::scripts_dir() {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
debug!("Could not determine scripts directory");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if !scripts_dir.exists() {
|
||||
debug!("Scripts directory not found at {:?}", scripts_dir);
|
||||
// Create the directory for the user
|
||||
if let Err(e) = fs::create_dir_all(&scripts_dir) {
|
||||
warn!("Failed to create scripts directory: {}", e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let entries = match fs::read_dir(&scripts_dir) {
|
||||
Ok(e) => e,
|
||||
Err(e) => {
|
||||
warn!("Failed to read scripts directory: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
|
||||
// Skip directories
|
||||
if path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if executable
|
||||
let metadata = match path.metadata() {
|
||||
Ok(m) => m,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
let is_executable = metadata.permissions().mode() & 0o111 != 0;
|
||||
if !is_executable {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get script name without extension
|
||||
let filename = path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
let name = path
|
||||
.file_stem()
|
||||
.map(|n| n.to_string_lossy().to_string())
|
||||
.unwrap_or(filename.clone());
|
||||
|
||||
// Try to read description from first line comment
|
||||
let description = Self::read_script_description(&path);
|
||||
|
||||
// Determine icon based on extension or shebang
|
||||
let icon = Self::determine_icon(&path);
|
||||
|
||||
self.items.push(LaunchItem {
|
||||
id: format!("script:{}", filename),
|
||||
name: format!("Script: {}", name),
|
||||
description,
|
||||
icon: Some(icon),
|
||||
provider: ProviderType::Scripts,
|
||||
command: path.to_string_lossy().to_string(),
|
||||
terminal: false,
|
||||
});
|
||||
}
|
||||
|
||||
debug!("Loaded {} scripts from {:?}", self.items.len(), scripts_dir);
|
||||
}
|
||||
|
||||
fn read_script_description(path: &PathBuf) -> Option<String> {
|
||||
let content = fs::read_to_string(path).ok()?;
|
||||
let mut lines = content.lines();
|
||||
|
||||
// Skip shebang if present
|
||||
let first_line = lines.next()?;
|
||||
let check_line = if first_line.starts_with("#!") {
|
||||
lines.next()?
|
||||
} else {
|
||||
first_line
|
||||
};
|
||||
|
||||
// Look for a comment description
|
||||
if check_line.starts_with("# ") {
|
||||
Some(check_line[2..].trim().to_string())
|
||||
} else if check_line.starts_with("// ") {
|
||||
Some(check_line[3..].trim().to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn determine_icon(path: &PathBuf) -> String {
|
||||
// Check extension first
|
||||
if let Some(ext) = path.extension() {
|
||||
match ext.to_string_lossy().as_ref() {
|
||||
"sh" | "bash" | "zsh" => return "utilities-terminal".to_string(),
|
||||
"py" | "python" => return "text-x-python".to_string(),
|
||||
"js" | "ts" => return "text-x-javascript".to_string(),
|
||||
"rb" => return "text-x-ruby".to_string(),
|
||||
"pl" => return "text-x-perl".to_string(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Check shebang
|
||||
if let Ok(content) = fs::read_to_string(path) {
|
||||
if let Some(first_line) = content.lines().next() {
|
||||
if first_line.contains("bash") || first_line.contains("sh") {
|
||||
return "utilities-terminal".to_string();
|
||||
} else if first_line.contains("python") {
|
||||
return "text-x-python".to_string();
|
||||
} else if first_line.contains("node") {
|
||||
return "text-x-javascript".to_string();
|
||||
} else if first_line.contains("ruby") {
|
||||
return "text-x-ruby".to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"application-x-executable".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl Provider for ScriptsProvider {
|
||||
fn name(&self) -> &str {
|
||||
"Scripts"
|
||||
}
|
||||
|
||||
fn provider_type(&self) -> ProviderType {
|
||||
ProviderType::Scripts
|
||||
}
|
||||
|
||||
fn refresh(&mut self) {
|
||||
self.load_scripts();
|
||||
}
|
||||
|
||||
fn items(&self) -> &[LaunchItem] {
|
||||
&self.items
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_scripts_provider() {
|
||||
let mut provider = ScriptsProvider::new();
|
||||
provider.refresh();
|
||||
// Just ensure it doesn't panic
|
||||
}
|
||||
}
|
||||
197
src/providers/ssh.rs
Normal file
197
src/providers/ssh.rs
Normal file
@@ -0,0 +1,197 @@
|
||||
use crate::providers::{LaunchItem, Provider, ProviderType};
|
||||
use log::{debug, warn};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// SSH connections provider - parses ~/.ssh/config
|
||||
pub struct SshProvider {
|
||||
items: Vec<LaunchItem>,
|
||||
terminal_command: String,
|
||||
}
|
||||
|
||||
impl SshProvider {
|
||||
#[allow(dead_code)]
|
||||
pub fn new() -> Self {
|
||||
Self::with_terminal("kitty")
|
||||
}
|
||||
|
||||
pub fn with_terminal(terminal: &str) -> Self {
|
||||
Self {
|
||||
items: Vec::new(),
|
||||
terminal_command: terminal.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn set_terminal(&mut self, terminal: &str) {
|
||||
self.terminal_command = terminal.to_string();
|
||||
}
|
||||
|
||||
fn ssh_config_path() -> Option<PathBuf> {
|
||||
dirs::home_dir().map(|p| p.join(".ssh").join("config"))
|
||||
}
|
||||
|
||||
fn parse_ssh_config(&mut self) {
|
||||
self.items.clear();
|
||||
|
||||
let config_path = match Self::ssh_config_path() {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
debug!("Could not determine SSH config path");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if !config_path.exists() {
|
||||
debug!("SSH config not found at {:?}", config_path);
|
||||
return;
|
||||
}
|
||||
|
||||
let content = match fs::read_to_string(&config_path) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
warn!("Failed to read SSH config: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut current_host: Option<String> = None;
|
||||
let mut current_hostname: Option<String> = None;
|
||||
let mut current_user: Option<String> = None;
|
||||
let mut current_port: Option<String> = None;
|
||||
|
||||
for line in content.lines() {
|
||||
let line = line.trim();
|
||||
|
||||
// Skip comments and empty lines
|
||||
if line.is_empty() || line.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Split on whitespace or '='
|
||||
let parts: Vec<&str> = line.splitn(2, |c: char| c.is_whitespace() || c == '=')
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
|
||||
if parts.len() < 2 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let key = parts[0].to_lowercase();
|
||||
let value = parts[1];
|
||||
|
||||
match key.as_str() {
|
||||
"host" => {
|
||||
// Save previous host if exists
|
||||
if let Some(host) = current_host.take() {
|
||||
self.add_host_item(
|
||||
&host,
|
||||
current_hostname.take(),
|
||||
current_user.take(),
|
||||
current_port.take(),
|
||||
);
|
||||
}
|
||||
|
||||
// Skip wildcards and patterns
|
||||
if !value.contains('*') && !value.contains('?') && value != "*" {
|
||||
current_host = Some(value.to_string());
|
||||
}
|
||||
current_hostname = None;
|
||||
current_user = None;
|
||||
current_port = None;
|
||||
}
|
||||
"hostname" => {
|
||||
current_hostname = Some(value.to_string());
|
||||
}
|
||||
"user" => {
|
||||
current_user = Some(value.to_string());
|
||||
}
|
||||
"port" => {
|
||||
current_port = Some(value.to_string());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Don't forget the last host
|
||||
if let Some(host) = current_host.take() {
|
||||
self.add_host_item(&host, current_hostname, current_user, current_port);
|
||||
}
|
||||
|
||||
debug!("Loaded {} SSH hosts", self.items.len());
|
||||
}
|
||||
|
||||
fn add_host_item(
|
||||
&mut self,
|
||||
host: &str,
|
||||
hostname: Option<String>,
|
||||
user: Option<String>,
|
||||
port: Option<String>,
|
||||
) {
|
||||
// Build description
|
||||
let mut desc_parts = Vec::new();
|
||||
if let Some(ref h) = hostname {
|
||||
desc_parts.push(h.clone());
|
||||
}
|
||||
if let Some(ref u) = user {
|
||||
desc_parts.push(format!("user: {}", u));
|
||||
}
|
||||
if let Some(ref p) = port {
|
||||
desc_parts.push(format!("port: {}", p));
|
||||
}
|
||||
|
||||
let description = if desc_parts.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(desc_parts.join(", "))
|
||||
};
|
||||
|
||||
// Build SSH command - just use the host alias, SSH will resolve the rest
|
||||
let ssh_command = format!("ssh {}", host);
|
||||
|
||||
// Wrap in terminal
|
||||
let command = format!("{} -e {}", self.terminal_command, ssh_command);
|
||||
|
||||
self.items.push(LaunchItem {
|
||||
id: format!("ssh:{}", host),
|
||||
name: format!("SSH: {}", host),
|
||||
description,
|
||||
icon: Some("utilities-terminal".to_string()),
|
||||
provider: ProviderType::Ssh,
|
||||
command,
|
||||
terminal: false, // We're already wrapping in terminal
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Provider for SshProvider {
|
||||
fn name(&self) -> &str {
|
||||
"SSH"
|
||||
}
|
||||
|
||||
fn provider_type(&self) -> ProviderType {
|
||||
ProviderType::Ssh
|
||||
}
|
||||
|
||||
fn refresh(&mut self) {
|
||||
self.parse_ssh_config();
|
||||
}
|
||||
|
||||
fn items(&self) -> &[LaunchItem] {
|
||||
&self.items
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_ssh_config() {
|
||||
// This test will only work if the user has an SSH config
|
||||
let mut provider = SshProvider::new();
|
||||
provider.refresh();
|
||||
// Just ensure it doesn't panic
|
||||
}
|
||||
}
|
||||
115
src/providers/system.rs
Normal file
115
src/providers/system.rs
Normal file
@@ -0,0 +1,115 @@
|
||||
use crate::providers::{LaunchItem, Provider, ProviderType};
|
||||
|
||||
/// System commands provider - shutdown, reboot, lock, etc.
|
||||
pub struct SystemProvider {
|
||||
items: Vec<LaunchItem>,
|
||||
}
|
||||
|
||||
impl SystemProvider {
|
||||
pub fn new() -> Self {
|
||||
Self { items: Vec::new() }
|
||||
}
|
||||
|
||||
fn load_commands(&mut self) {
|
||||
self.items.clear();
|
||||
|
||||
// Define system commands
|
||||
// Format: (id, name, description, icon, command)
|
||||
let commands: Vec<(&str, &str, &str, &str, &str)> = vec![
|
||||
(
|
||||
"system:shutdown",
|
||||
"Shutdown",
|
||||
"Power off the system",
|
||||
"system-shutdown",
|
||||
"systemctl poweroff",
|
||||
),
|
||||
(
|
||||
"system:reboot",
|
||||
"Reboot",
|
||||
"Restart the system",
|
||||
"system-reboot",
|
||||
"systemctl reboot",
|
||||
),
|
||||
(
|
||||
"system:reboot-bios",
|
||||
"Reboot into BIOS",
|
||||
"Restart into UEFI/BIOS setup",
|
||||
"system-reboot",
|
||||
"systemctl reboot --firmware-setup",
|
||||
),
|
||||
(
|
||||
"system:suspend",
|
||||
"Suspend",
|
||||
"Suspend to RAM",
|
||||
"system-suspend",
|
||||
"systemctl suspend",
|
||||
),
|
||||
(
|
||||
"system:hibernate",
|
||||
"Hibernate",
|
||||
"Suspend to disk",
|
||||
"system-suspend-hibernate",
|
||||
"systemctl hibernate",
|
||||
),
|
||||
(
|
||||
"system:lock",
|
||||
"Lock Screen",
|
||||
"Lock the session",
|
||||
"system-lock-screen",
|
||||
"loginctl lock-session",
|
||||
),
|
||||
(
|
||||
"system:logout",
|
||||
"Log Out",
|
||||
"End the current session",
|
||||
"system-log-out",
|
||||
"loginctl terminate-session self",
|
||||
),
|
||||
];
|
||||
|
||||
for (id, name, description, icon, command) in commands {
|
||||
self.items.push(LaunchItem {
|
||||
id: id.to_string(),
|
||||
name: name.to_string(),
|
||||
description: Some(description.to_string()),
|
||||
icon: Some(icon.to_string()),
|
||||
provider: ProviderType::System,
|
||||
command: command.to_string(),
|
||||
terminal: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Provider for SystemProvider {
|
||||
fn name(&self) -> &str {
|
||||
"System"
|
||||
}
|
||||
|
||||
fn provider_type(&self) -> ProviderType {
|
||||
ProviderType::System
|
||||
}
|
||||
|
||||
fn refresh(&mut self) {
|
||||
self.load_commands();
|
||||
}
|
||||
|
||||
fn items(&self) -> &[LaunchItem] {
|
||||
&self.items
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_system_provider() {
|
||||
let mut provider = SystemProvider::new();
|
||||
provider.refresh();
|
||||
|
||||
assert!(provider.items().len() >= 6);
|
||||
assert!(provider.items().iter().any(|i| i.name == "Shutdown"));
|
||||
assert!(provider.items().iter().any(|i| i.name == "Reboot into BIOS"));
|
||||
}
|
||||
}
|
||||
195
src/providers/websearch.rs
Normal file
195
src/providers/websearch.rs
Normal file
@@ -0,0 +1,195 @@
|
||||
use crate::providers::{LaunchItem, ProviderType};
|
||||
|
||||
/// Common search engine URL templates
|
||||
/// {query} is replaced with the URL-encoded search term
|
||||
pub const SEARCH_ENGINES: &[(&str, &str)] = &[
|
||||
("google", "https://www.google.com/search?q={query}"),
|
||||
("duckduckgo", "https://duckduckgo.com/?q={query}"),
|
||||
("bing", "https://www.bing.com/search?q={query}"),
|
||||
("startpage", "https://www.startpage.com/search?q={query}"),
|
||||
("searxng", "https://searx.be/search?q={query}"),
|
||||
("brave", "https://search.brave.com/search?q={query}"),
|
||||
("ecosia", "https://www.ecosia.org/search?q={query}"),
|
||||
];
|
||||
|
||||
/// Default search engine if not configured
|
||||
pub const DEFAULT_ENGINE: &str = "duckduckgo";
|
||||
|
||||
/// Web search provider - opens browser with search query
|
||||
pub struct WebSearchProvider {
|
||||
/// URL template with {query} placeholder
|
||||
url_template: String,
|
||||
}
|
||||
|
||||
impl WebSearchProvider {
|
||||
#[allow(dead_code)]
|
||||
pub fn new() -> Self {
|
||||
Self::with_engine(DEFAULT_ENGINE)
|
||||
}
|
||||
|
||||
/// Create provider with specific search engine
|
||||
pub fn with_engine(engine_name: &str) -> Self {
|
||||
let url_template = SEARCH_ENGINES
|
||||
.iter()
|
||||
.find(|(name, _)| *name == engine_name.to_lowercase())
|
||||
.map(|(_, url)| url.to_string())
|
||||
.unwrap_or_else(|| {
|
||||
// If not a known engine, treat it as a custom URL template
|
||||
if engine_name.contains("{query}") {
|
||||
engine_name.to_string()
|
||||
} else {
|
||||
// Fall back to default
|
||||
SEARCH_ENGINES
|
||||
.iter()
|
||||
.find(|(name, _)| *name == DEFAULT_ENGINE)
|
||||
.map(|(_, url)| url.to_string())
|
||||
.unwrap()
|
||||
}
|
||||
});
|
||||
|
||||
Self { url_template }
|
||||
}
|
||||
|
||||
/// Check if query is a web search query
|
||||
/// Triggers on: `? query`, `web query`, `search query`
|
||||
pub fn is_websearch_query(query: &str) -> bool {
|
||||
let trimmed = query.trim();
|
||||
trimmed.starts_with("? ")
|
||||
|| trimmed.starts_with("?")
|
||||
|| trimmed.to_lowercase().starts_with("web ")
|
||||
|| trimmed.to_lowercase().starts_with("search ")
|
||||
}
|
||||
|
||||
/// Extract the search term from the query
|
||||
fn extract_search_term(query: &str) -> Option<&str> {
|
||||
let trimmed = query.trim();
|
||||
|
||||
if let Some(rest) = trimmed.strip_prefix("? ") {
|
||||
Some(rest.trim())
|
||||
} else if let Some(rest) = trimmed.strip_prefix("?") {
|
||||
Some(rest.trim())
|
||||
} else if trimmed.to_lowercase().starts_with("web ") {
|
||||
// Need to get the original casing
|
||||
Some(trimmed[4..].trim())
|
||||
} else if trimmed.to_lowercase().starts_with("search ") {
|
||||
Some(trimmed[7..].trim())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// URL-encode a search query
|
||||
fn url_encode(query: &str) -> String {
|
||||
// TODO: This is where you can implement the URL encoding logic!
|
||||
// Consider: Should we use a crate like `urlencoding` or implement manually?
|
||||
// Manual encoding needs to handle: spaces, &, =, ?, #, etc.
|
||||
query
|
||||
.chars()
|
||||
.map(|c| match c {
|
||||
' ' => "+".to_string(),
|
||||
'&' => "%26".to_string(),
|
||||
'=' => "%3D".to_string(),
|
||||
'?' => "%3F".to_string(),
|
||||
'#' => "%23".to_string(),
|
||||
'+' => "%2B".to_string(),
|
||||
'%' => "%25".to_string(),
|
||||
c if c.is_ascii_alphanumeric() || "-_.~".contains(c) => c.to_string(),
|
||||
c => format!("%{:02X}", c as u32),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Build the search URL from a query
|
||||
fn build_search_url(&self, search_term: &str) -> String {
|
||||
let encoded = Self::url_encode(search_term);
|
||||
self.url_template.replace("{query}", &encoded)
|
||||
}
|
||||
|
||||
/// Evaluate a web search query and return a LaunchItem if valid
|
||||
pub fn evaluate(&self, query: &str) -> Option<LaunchItem> {
|
||||
let search_term = Self::extract_search_term(query)?;
|
||||
|
||||
if search_term.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
self.evaluate_raw(search_term)
|
||||
}
|
||||
|
||||
/// Evaluate a raw search term (for :web filter mode)
|
||||
pub fn evaluate_raw(&self, search_term: &str) -> Option<LaunchItem> {
|
||||
let trimmed = search_term.trim();
|
||||
if trimmed.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let url = self.build_search_url(trimmed);
|
||||
|
||||
// Use xdg-open to open the browser
|
||||
let command = format!("xdg-open '{}'", url);
|
||||
|
||||
Some(LaunchItem {
|
||||
id: format!("websearch:{}", trimmed),
|
||||
name: format!("Search: {}", trimmed),
|
||||
description: Some("Open in browser".to_string()),
|
||||
icon: Some("web-browser".to_string()),
|
||||
provider: ProviderType::WebSearch,
|
||||
command,
|
||||
terminal: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_is_websearch_query() {
|
||||
assert!(WebSearchProvider::is_websearch_query("? rust programming"));
|
||||
assert!(WebSearchProvider::is_websearch_query("?rust"));
|
||||
assert!(WebSearchProvider::is_websearch_query("web rust"));
|
||||
assert!(WebSearchProvider::is_websearch_query("search rust"));
|
||||
assert!(!WebSearchProvider::is_websearch_query("rust"));
|
||||
assert!(!WebSearchProvider::is_websearch_query("= 5+3"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_search_term() {
|
||||
assert_eq!(
|
||||
WebSearchProvider::extract_search_term("? rust programming"),
|
||||
Some("rust programming")
|
||||
);
|
||||
assert_eq!(
|
||||
WebSearchProvider::extract_search_term("?rust"),
|
||||
Some("rust")
|
||||
);
|
||||
assert_eq!(
|
||||
WebSearchProvider::extract_search_term("web rust docs"),
|
||||
Some("rust docs")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_url_encode() {
|
||||
assert_eq!(WebSearchProvider::url_encode("hello world"), "hello+world");
|
||||
assert_eq!(WebSearchProvider::url_encode("foo&bar"), "foo%26bar");
|
||||
assert_eq!(WebSearchProvider::url_encode("a=b"), "a%3Db");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_search_url() {
|
||||
let provider = WebSearchProvider::with_engine("duckduckgo");
|
||||
let url = provider.build_search_url("rust programming");
|
||||
assert_eq!(url, "https://duckduckgo.com/?q=rust+programming");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_evaluate() {
|
||||
let provider = WebSearchProvider::new();
|
||||
let item = provider.evaluate("? rust docs").unwrap();
|
||||
assert_eq!(item.name, "Search: rust docs");
|
||||
assert!(item.command.contains("xdg-open"));
|
||||
assert!(item.command.contains("duckduckgo"));
|
||||
}
|
||||
}
|
||||
27
src/theme.rs
27
src/theme.rs
@@ -35,15 +35,42 @@ pub fn generate_variables_css(config: &AppearanceConfig) -> String {
|
||||
if let Some(ref badge_app) = config.colors.badge_app {
|
||||
css.push_str(&format!(" --owlry-badge-app: {};\n", badge_app));
|
||||
}
|
||||
if let Some(ref badge_bookmark) = config.colors.badge_bookmark {
|
||||
css.push_str(&format!(" --owlry-badge-bookmark: {};\n", badge_bookmark));
|
||||
}
|
||||
if let Some(ref badge_calc) = config.colors.badge_calc {
|
||||
css.push_str(&format!(" --owlry-badge-calc: {};\n", badge_calc));
|
||||
}
|
||||
if let Some(ref badge_clip) = config.colors.badge_clip {
|
||||
css.push_str(&format!(" --owlry-badge-clip: {};\n", badge_clip));
|
||||
}
|
||||
if let Some(ref badge_cmd) = config.colors.badge_cmd {
|
||||
css.push_str(&format!(" --owlry-badge-cmd: {};\n", badge_cmd));
|
||||
}
|
||||
if let Some(ref badge_dmenu) = config.colors.badge_dmenu {
|
||||
css.push_str(&format!(" --owlry-badge-dmenu: {};\n", badge_dmenu));
|
||||
}
|
||||
if let Some(ref badge_emoji) = config.colors.badge_emoji {
|
||||
css.push_str(&format!(" --owlry-badge-emoji: {};\n", badge_emoji));
|
||||
}
|
||||
if let Some(ref badge_file) = config.colors.badge_file {
|
||||
css.push_str(&format!(" --owlry-badge-file: {};\n", badge_file));
|
||||
}
|
||||
if let Some(ref badge_script) = config.colors.badge_script {
|
||||
css.push_str(&format!(" --owlry-badge-script: {};\n", badge_script));
|
||||
}
|
||||
if let Some(ref badge_ssh) = config.colors.badge_ssh {
|
||||
css.push_str(&format!(" --owlry-badge-ssh: {};\n", badge_ssh));
|
||||
}
|
||||
if let Some(ref badge_sys) = config.colors.badge_sys {
|
||||
css.push_str(&format!(" --owlry-badge-sys: {};\n", badge_sys));
|
||||
}
|
||||
if let Some(ref badge_uuctl) = config.colors.badge_uuctl {
|
||||
css.push_str(&format!(" --owlry-badge-uuctl: {};\n", badge_uuctl));
|
||||
}
|
||||
if let Some(ref badge_web) = config.colors.badge_web {
|
||||
css.push_str(&format!(" --owlry-badge-web: {};\n", badge_web));
|
||||
}
|
||||
|
||||
css.push_str("}\n");
|
||||
css
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::config::Config;
|
||||
use crate::data::FrecencyStore;
|
||||
use crate::filter::ProviderFilter;
|
||||
use crate::providers::{LaunchItem, ProviderManager, ProviderType, UuctlProvider};
|
||||
use crate::ui::ResultRow;
|
||||
@@ -36,6 +37,7 @@ pub struct MainWindow {
|
||||
scrolled: ScrolledWindow,
|
||||
config: Rc<RefCell<Config>>,
|
||||
providers: Rc<RefCell<ProviderManager>>,
|
||||
frecency: Rc<RefCell<FrecencyStore>>,
|
||||
current_results: Rc<RefCell<Vec<LaunchItem>>>,
|
||||
filter: Rc<RefCell<ProviderFilter>>,
|
||||
mode_label: Label,
|
||||
@@ -49,6 +51,7 @@ impl MainWindow {
|
||||
app: &Application,
|
||||
config: Rc<RefCell<Config>>,
|
||||
providers: Rc<RefCell<ProviderManager>>,
|
||||
frecency: Rc<RefCell<FrecencyStore>>,
|
||||
filter: Rc<RefCell<ProviderFilter>>,
|
||||
) -> Self {
|
||||
let cfg = config.borrow();
|
||||
@@ -140,7 +143,7 @@ impl MainWindow {
|
||||
hints_box.add_css_class("owlry-hints");
|
||||
|
||||
let hints_label = Label::builder()
|
||||
.label("Tab: cycle mode ↑↓: navigate Enter: launch Esc: close :app :cmd :uuctl")
|
||||
.label("Tab: cycle mode ↑↓: navigate Enter: launch Esc: close = calc ? web :app :cmd")
|
||||
.halign(gtk4::Align::Center)
|
||||
.hexpand(true)
|
||||
.build();
|
||||
@@ -163,6 +166,7 @@ impl MainWindow {
|
||||
scrolled,
|
||||
config,
|
||||
providers,
|
||||
frecency,
|
||||
current_results: Rc::new(RefCell::new(Vec::new())),
|
||||
filter,
|
||||
mode_label,
|
||||
@@ -202,9 +206,18 @@ impl MainWindow {
|
||||
button.add_css_class("owlry-filter-button");
|
||||
let css_class = match provider_type {
|
||||
ProviderType::Application => "owlry-filter-app",
|
||||
ProviderType::Bookmarks => "owlry-filter-bookmark",
|
||||
ProviderType::Calculator => "owlry-filter-calc",
|
||||
ProviderType::Clipboard => "owlry-filter-clip",
|
||||
ProviderType::Command => "owlry-filter-cmd",
|
||||
ProviderType::Uuctl => "owlry-filter-uuctl",
|
||||
ProviderType::Dmenu => "owlry-filter-dmenu",
|
||||
ProviderType::Emoji => "owlry-filter-emoji",
|
||||
ProviderType::Files => "owlry-filter-file",
|
||||
ProviderType::Scripts => "owlry-filter-script",
|
||||
ProviderType::Ssh => "owlry-filter-ssh",
|
||||
ProviderType::System => "owlry-filter-sys",
|
||||
ProviderType::Uuctl => "owlry-filter-uuctl",
|
||||
ProviderType::WebSearch => "owlry-filter-web",
|
||||
};
|
||||
button.add_css_class(css_class);
|
||||
|
||||
@@ -221,9 +234,18 @@ impl MainWindow {
|
||||
.iter()
|
||||
.map(|p| match p {
|
||||
ProviderType::Application => "applications",
|
||||
ProviderType::Bookmarks => "bookmarks",
|
||||
ProviderType::Calculator => "calculator",
|
||||
ProviderType::Clipboard => "clipboard",
|
||||
ProviderType::Command => "commands",
|
||||
ProviderType::Uuctl => "uuctl units",
|
||||
ProviderType::Dmenu => "options",
|
||||
ProviderType::Emoji => "emoji",
|
||||
ProviderType::Files => "files",
|
||||
ProviderType::Scripts => "scripts",
|
||||
ProviderType::Ssh => "SSH hosts",
|
||||
ProviderType::System => "system",
|
||||
ProviderType::Uuctl => "uuctl units",
|
||||
ProviderType::WebSearch => "web",
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -328,7 +350,7 @@ impl MainWindow {
|
||||
|
||||
// Restore UI
|
||||
mode_label.set_label(filter.borrow().mode_display_name());
|
||||
hints_label.set_label("Tab: cycle mode ↑↓: navigate Enter: launch Esc: close :app :cmd :uuctl");
|
||||
hints_label.set_label("Tab: cycle mode ↑↓: navigate Enter: launch Esc: close = calc ? web :app :cmd");
|
||||
search_entry.set_placeholder_text(Some(&Self::build_placeholder(&filter.borrow())));
|
||||
search_entry.set_text(&saved_search);
|
||||
|
||||
@@ -341,6 +363,7 @@ impl MainWindow {
|
||||
let providers = self.providers.clone();
|
||||
let results_list = self.results_list.clone();
|
||||
let config = self.config.clone();
|
||||
let frecency = self.frecency.clone();
|
||||
let current_results = self.current_results.clone();
|
||||
let filter = self.filter.clone();
|
||||
let mode_label = self.mode_label.clone();
|
||||
@@ -401,21 +424,44 @@ impl MainWindow {
|
||||
if parsed.prefix.is_some() {
|
||||
let prefix_name = match parsed.prefix.unwrap() {
|
||||
ProviderType::Application => "applications",
|
||||
ProviderType::Bookmarks => "bookmarks",
|
||||
ProviderType::Calculator => "calculator",
|
||||
ProviderType::Clipboard => "clipboard",
|
||||
ProviderType::Command => "commands",
|
||||
ProviderType::Uuctl => "uuctl units",
|
||||
ProviderType::Dmenu => "options",
|
||||
ProviderType::Emoji => "emoji",
|
||||
ProviderType::Files => "files",
|
||||
ProviderType::Scripts => "scripts",
|
||||
ProviderType::Ssh => "SSH hosts",
|
||||
ProviderType::System => "system",
|
||||
ProviderType::Uuctl => "uuctl units",
|
||||
ProviderType::WebSearch => "web",
|
||||
};
|
||||
search_entry_for_change
|
||||
.set_placeholder_text(Some(&format!("Search {}...", prefix_name)));
|
||||
}
|
||||
|
||||
let max_results = config.borrow().general.max_results;
|
||||
let results: Vec<LaunchItem> = providers
|
||||
.borrow()
|
||||
.search_filtered(&parsed.query, max_results, &filter.borrow())
|
||||
.into_iter()
|
||||
.map(|(item, _)| item)
|
||||
.collect();
|
||||
let cfg = config.borrow();
|
||||
let max_results = cfg.general.max_results;
|
||||
let frecency_weight = cfg.providers.frecency_weight;
|
||||
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)
|
||||
.into_iter()
|
||||
.map(|(item, _)| item)
|
||||
.collect()
|
||||
} else {
|
||||
providers
|
||||
.borrow()
|
||||
.search_filtered(&parsed.query, max_results, &filter.borrow())
|
||||
.into_iter()
|
||||
.map(|(item, _)| item)
|
||||
.collect()
|
||||
};
|
||||
|
||||
while let Some(child) = results_list.first_child() {
|
||||
results_list.remove(&child);
|
||||
@@ -437,6 +483,7 @@ impl MainWindow {
|
||||
let results_list_for_activate = self.results_list.clone();
|
||||
let current_results_for_activate = self.current_results.clone();
|
||||
let config_for_activate = self.config.clone();
|
||||
let frecency_for_activate = self.frecency.clone();
|
||||
let window_for_activate = self.window.clone();
|
||||
let submenu_state_for_activate = self.submenu_state.clone();
|
||||
let mode_label_for_activate = self.mode_label.clone();
|
||||
@@ -470,7 +517,7 @@ impl MainWindow {
|
||||
);
|
||||
} else {
|
||||
// Execute the command
|
||||
Self::launch_item(item, &config_for_activate.borrow());
|
||||
Self::launch_item(item, &config_for_activate.borrow(), &frecency_for_activate);
|
||||
window_for_activate.close();
|
||||
}
|
||||
}
|
||||
@@ -647,6 +694,7 @@ impl MainWindow {
|
||||
// Double-click to launch
|
||||
let current_results = self.current_results.clone();
|
||||
let config = self.config.clone();
|
||||
let frecency = self.frecency.clone();
|
||||
let window = self.window.clone();
|
||||
let submenu_state = self.submenu_state.clone();
|
||||
let results_list_for_click = self.results_list.clone();
|
||||
@@ -675,7 +723,7 @@ impl MainWindow {
|
||||
is_active,
|
||||
);
|
||||
} else {
|
||||
Self::launch_item(item, &config.borrow());
|
||||
Self::launch_item(item, &config.borrow(), &frecency);
|
||||
window.close();
|
||||
}
|
||||
}
|
||||
@@ -743,14 +791,27 @@ impl MainWindow {
|
||||
}
|
||||
|
||||
fn update_results(&self, query: &str) {
|
||||
let max_results = self.config.borrow().general.max_results;
|
||||
let results: Vec<LaunchItem> = self
|
||||
.providers
|
||||
.borrow()
|
||||
.search_filtered(query, max_results, &self.filter.borrow())
|
||||
.into_iter()
|
||||
.map(|(item, _)| item)
|
||||
.collect();
|
||||
let cfg = self.config.borrow();
|
||||
let max_results = cfg.general.max_results;
|
||||
let frecency_weight = cfg.providers.frecency_weight;
|
||||
let use_frecency = cfg.providers.frecency;
|
||||
drop(cfg);
|
||||
|
||||
let results: Vec<LaunchItem> = if use_frecency {
|
||||
self.providers
|
||||
.borrow_mut()
|
||||
.search_with_frecency(query, max_results, &self.filter.borrow(), &self.frecency.borrow(), frecency_weight)
|
||||
.into_iter()
|
||||
.map(|(item, _)| item)
|
||||
.collect()
|
||||
} else {
|
||||
self.providers
|
||||
.borrow()
|
||||
.search_filtered(query, max_results, &self.filter.borrow())
|
||||
.into_iter()
|
||||
.map(|(item, _)| item)
|
||||
.collect()
|
||||
};
|
||||
|
||||
while let Some(child) = self.results_list.first_child() {
|
||||
self.results_list.remove(&child);
|
||||
@@ -768,7 +829,12 @@ impl MainWindow {
|
||||
*self.current_results.borrow_mut() = results;
|
||||
}
|
||||
|
||||
fn launch_item(item: &LaunchItem, config: &Config) {
|
||||
fn launch_item(item: &LaunchItem, config: &Config, frecency: &Rc<RefCell<FrecencyStore>>) {
|
||||
// Record this launch for frecency tracking
|
||||
if config.providers.frecency {
|
||||
frecency.borrow_mut().record_launch(&item.id);
|
||||
}
|
||||
|
||||
info!("Launching: {} ({})", item.name, item.command);
|
||||
|
||||
let cmd = if item.terminal {
|
||||
|
||||
@@ -32,9 +32,18 @@ impl ResultRow {
|
||||
// Default icon based on provider type
|
||||
let default_icon = match item.provider {
|
||||
crate::providers::ProviderType::Application => "application-x-executable",
|
||||
crate::providers::ProviderType::Bookmarks => "user-bookmarks",
|
||||
crate::providers::ProviderType::Calculator => "accessories-calculator",
|
||||
crate::providers::ProviderType::Clipboard => "edit-paste",
|
||||
crate::providers::ProviderType::Command => "utilities-terminal",
|
||||
crate::providers::ProviderType::Dmenu => "view-list-symbolic",
|
||||
crate::providers::ProviderType::Emoji => "face-smile",
|
||||
crate::providers::ProviderType::Files => "folder",
|
||||
crate::providers::ProviderType::Scripts => "application-x-executable",
|
||||
crate::providers::ProviderType::Ssh => "network-server",
|
||||
crate::providers::ProviderType::System => "system-shutdown",
|
||||
crate::providers::ProviderType::Uuctl => "system-run",
|
||||
crate::providers::ProviderType::WebSearch => "web-browser",
|
||||
};
|
||||
Image::from_icon_name(default_icon)
|
||||
};
|
||||
|
||||
114
themes/catppuccin-mocha.css
Normal file
114
themes/catppuccin-mocha.css
Normal file
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Owlry - Catppuccin Mocha Theme
|
||||
* A soothing pastel theme based on Catppuccin Mocha palette
|
||||
* https://catppuccin.com/
|
||||
*
|
||||
* Usage: Copy to ~/.config/owlry/themes/catppuccin-mocha.css
|
||||
* Set theme = "catppuccin-mocha" in config.toml
|
||||
*/
|
||||
|
||||
:root {
|
||||
--owlry-bg: #1e1e2e;
|
||||
--owlry-bg-secondary: #313244;
|
||||
--owlry-border: #45475a;
|
||||
--owlry-text: #cdd6f4;
|
||||
--owlry-text-secondary: #a6adc8;
|
||||
--owlry-accent: #cba6f7;
|
||||
--owlry-accent-bright: #f5c2e7;
|
||||
--owlry-badge-app: #a6e3a1;
|
||||
--owlry-badge-cmd: #89b4fa;
|
||||
--owlry-badge-dmenu: #f9e2af;
|
||||
--owlry-badge-uuctl: #fab387;
|
||||
}
|
||||
|
||||
.owlry-main {
|
||||
background-color: rgba(30, 30, 46, 0.95);
|
||||
border: 1px solid rgba(69, 71, 90, 0.6);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5),
|
||||
0 0 0 1px rgba(203, 166, 247, 0.1);
|
||||
}
|
||||
|
||||
.owlry-search {
|
||||
background-color: rgba(49, 50, 68, 0.8);
|
||||
border: 2px solid rgba(69, 71, 90, 0.5);
|
||||
color: var(--owlry-text);
|
||||
caret-color: var(--owlry-accent);
|
||||
}
|
||||
|
||||
.owlry-search:focus {
|
||||
border-color: var(--owlry-accent);
|
||||
box-shadow: 0 0 0 2px rgba(203, 166, 247, 0.2);
|
||||
}
|
||||
|
||||
.owlry-result-row:hover {
|
||||
background-color: rgba(49, 50, 68, 0.6);
|
||||
}
|
||||
|
||||
.owlry-result-row:selected {
|
||||
background-color: rgba(203, 166, 247, 0.15);
|
||||
border-left: 3px solid var(--owlry-accent);
|
||||
}
|
||||
|
||||
.owlry-result-row:selected .owlry-result-name {
|
||||
color: var(--owlry-accent-bright);
|
||||
}
|
||||
|
||||
.owlry-result-row:selected .owlry-result-icon {
|
||||
color: var(--owlry-accent);
|
||||
}
|
||||
|
||||
.owlry-badge-app {
|
||||
background-color: rgba(166, 227, 161, 0.2);
|
||||
color: var(--owlry-badge-app);
|
||||
}
|
||||
|
||||
.owlry-badge-cmd {
|
||||
background-color: rgba(137, 180, 250, 0.2);
|
||||
color: var(--owlry-badge-cmd);
|
||||
}
|
||||
|
||||
.owlry-badge-dmenu {
|
||||
background-color: rgba(249, 226, 175, 0.2);
|
||||
color: var(--owlry-badge-dmenu);
|
||||
}
|
||||
|
||||
.owlry-badge-uuctl {
|
||||
background-color: rgba(250, 179, 135, 0.2);
|
||||
color: var(--owlry-badge-uuctl);
|
||||
}
|
||||
|
||||
.owlry-filter-button:checked {
|
||||
background-color: rgba(203, 166, 247, 0.2);
|
||||
color: var(--owlry-accent);
|
||||
border-color: rgba(203, 166, 247, 0.4);
|
||||
}
|
||||
|
||||
.owlry-filter-app:checked {
|
||||
background-color: rgba(166, 227, 161, 0.2);
|
||||
color: var(--owlry-badge-app);
|
||||
border-color: rgba(166, 227, 161, 0.4);
|
||||
}
|
||||
|
||||
.owlry-filter-cmd:checked {
|
||||
background-color: rgba(137, 180, 250, 0.2);
|
||||
color: var(--owlry-badge-cmd);
|
||||
border-color: rgba(137, 180, 250, 0.4);
|
||||
}
|
||||
|
||||
.owlry-filter-uuctl:checked {
|
||||
background-color: rgba(250, 179, 135, 0.2);
|
||||
color: var(--owlry-badge-uuctl);
|
||||
border-color: rgba(250, 179, 135, 0.4);
|
||||
}
|
||||
|
||||
scrollbar slider {
|
||||
background-color: rgba(69, 71, 90, 0.5);
|
||||
}
|
||||
|
||||
scrollbar slider:hover {
|
||||
background-color: rgba(88, 91, 112, 0.7);
|
||||
}
|
||||
|
||||
scrollbar slider:active {
|
||||
background-color: var(--owlry-accent);
|
||||
}
|
||||
114
themes/dracula.css
Normal file
114
themes/dracula.css
Normal file
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Owlry - Dracula Theme
|
||||
* A dark theme for vampires
|
||||
* https://draculatheme.com/
|
||||
*
|
||||
* Usage: Copy to ~/.config/owlry/themes/dracula.css
|
||||
* Set theme = "dracula" in config.toml
|
||||
*/
|
||||
|
||||
:root {
|
||||
--owlry-bg: #282a36;
|
||||
--owlry-bg-secondary: #44475a;
|
||||
--owlry-border: #6272a4;
|
||||
--owlry-text: #f8f8f2;
|
||||
--owlry-text-secondary: #6272a4;
|
||||
--owlry-accent: #bd93f9;
|
||||
--owlry-accent-bright: #ff79c6;
|
||||
--owlry-badge-app: #50fa7b;
|
||||
--owlry-badge-cmd: #8be9fd;
|
||||
--owlry-badge-dmenu: #f1fa8c;
|
||||
--owlry-badge-uuctl: #ffb86c;
|
||||
}
|
||||
|
||||
.owlry-main {
|
||||
background-color: rgba(40, 42, 54, 0.95);
|
||||
border: 1px solid rgba(98, 114, 164, 0.6);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5),
|
||||
0 0 0 1px rgba(189, 147, 249, 0.1);
|
||||
}
|
||||
|
||||
.owlry-search {
|
||||
background-color: rgba(68, 71, 90, 0.8);
|
||||
border: 2px solid rgba(98, 114, 164, 0.5);
|
||||
color: var(--owlry-text);
|
||||
caret-color: var(--owlry-accent);
|
||||
}
|
||||
|
||||
.owlry-search:focus {
|
||||
border-color: var(--owlry-accent);
|
||||
box-shadow: 0 0 0 2px rgba(189, 147, 249, 0.2);
|
||||
}
|
||||
|
||||
.owlry-result-row:hover {
|
||||
background-color: rgba(68, 71, 90, 0.6);
|
||||
}
|
||||
|
||||
.owlry-result-row:selected {
|
||||
background-color: rgba(189, 147, 249, 0.15);
|
||||
border-left: 3px solid var(--owlry-accent);
|
||||
}
|
||||
|
||||
.owlry-result-row:selected .owlry-result-name {
|
||||
color: var(--owlry-accent-bright);
|
||||
}
|
||||
|
||||
.owlry-result-row:selected .owlry-result-icon {
|
||||
color: var(--owlry-accent);
|
||||
}
|
||||
|
||||
.owlry-badge-app {
|
||||
background-color: rgba(80, 250, 123, 0.2);
|
||||
color: var(--owlry-badge-app);
|
||||
}
|
||||
|
||||
.owlry-badge-cmd {
|
||||
background-color: rgba(139, 233, 253, 0.2);
|
||||
color: var(--owlry-badge-cmd);
|
||||
}
|
||||
|
||||
.owlry-badge-dmenu {
|
||||
background-color: rgba(241, 250, 140, 0.2);
|
||||
color: var(--owlry-badge-dmenu);
|
||||
}
|
||||
|
||||
.owlry-badge-uuctl {
|
||||
background-color: rgba(255, 184, 108, 0.2);
|
||||
color: var(--owlry-badge-uuctl);
|
||||
}
|
||||
|
||||
.owlry-filter-button:checked {
|
||||
background-color: rgba(189, 147, 249, 0.2);
|
||||
color: var(--owlry-accent);
|
||||
border-color: rgba(189, 147, 249, 0.4);
|
||||
}
|
||||
|
||||
.owlry-filter-app:checked {
|
||||
background-color: rgba(80, 250, 123, 0.2);
|
||||
color: var(--owlry-badge-app);
|
||||
border-color: rgba(80, 250, 123, 0.4);
|
||||
}
|
||||
|
||||
.owlry-filter-cmd:checked {
|
||||
background-color: rgba(139, 233, 253, 0.2);
|
||||
color: var(--owlry-badge-cmd);
|
||||
border-color: rgba(139, 233, 253, 0.4);
|
||||
}
|
||||
|
||||
.owlry-filter-uuctl:checked {
|
||||
background-color: rgba(255, 184, 108, 0.2);
|
||||
color: var(--owlry-badge-uuctl);
|
||||
border-color: rgba(255, 184, 108, 0.4);
|
||||
}
|
||||
|
||||
scrollbar slider {
|
||||
background-color: rgba(98, 114, 164, 0.5);
|
||||
}
|
||||
|
||||
scrollbar slider:hover {
|
||||
background-color: rgba(98, 114, 164, 0.7);
|
||||
}
|
||||
|
||||
scrollbar slider:active {
|
||||
background-color: var(--owlry-accent);
|
||||
}
|
||||
114
themes/gruvbox-dark.css
Normal file
114
themes/gruvbox-dark.css
Normal file
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Owlry - Gruvbox Dark Theme
|
||||
* Retro groove color scheme
|
||||
* https://github.com/morhetz/gruvbox
|
||||
*
|
||||
* Usage: Copy to ~/.config/owlry/themes/gruvbox-dark.css
|
||||
* Set theme = "gruvbox-dark" in config.toml
|
||||
*/
|
||||
|
||||
:root {
|
||||
--owlry-bg: #282828;
|
||||
--owlry-bg-secondary: #3c3836;
|
||||
--owlry-border: #504945;
|
||||
--owlry-text: #ebdbb2;
|
||||
--owlry-text-secondary: #a89984;
|
||||
--owlry-accent: #fe8019;
|
||||
--owlry-accent-bright: #fabd2f;
|
||||
--owlry-badge-app: #b8bb26;
|
||||
--owlry-badge-cmd: #83a598;
|
||||
--owlry-badge-dmenu: #fabd2f;
|
||||
--owlry-badge-uuctl: #fb4934;
|
||||
}
|
||||
|
||||
.owlry-main {
|
||||
background-color: rgba(40, 40, 40, 0.95);
|
||||
border: 1px solid rgba(80, 73, 69, 0.6);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5),
|
||||
0 0 0 1px rgba(254, 128, 25, 0.1);
|
||||
}
|
||||
|
||||
.owlry-search {
|
||||
background-color: rgba(60, 56, 54, 0.8);
|
||||
border: 2px solid rgba(80, 73, 69, 0.5);
|
||||
color: var(--owlry-text);
|
||||
caret-color: var(--owlry-accent);
|
||||
}
|
||||
|
||||
.owlry-search:focus {
|
||||
border-color: var(--owlry-accent);
|
||||
box-shadow: 0 0 0 2px rgba(254, 128, 25, 0.2);
|
||||
}
|
||||
|
||||
.owlry-result-row:hover {
|
||||
background-color: rgba(60, 56, 54, 0.6);
|
||||
}
|
||||
|
||||
.owlry-result-row:selected {
|
||||
background-color: rgba(254, 128, 25, 0.15);
|
||||
border-left: 3px solid var(--owlry-accent);
|
||||
}
|
||||
|
||||
.owlry-result-row:selected .owlry-result-name {
|
||||
color: var(--owlry-accent-bright);
|
||||
}
|
||||
|
||||
.owlry-result-row:selected .owlry-result-icon {
|
||||
color: var(--owlry-accent);
|
||||
}
|
||||
|
||||
.owlry-badge-app {
|
||||
background-color: rgba(184, 187, 38, 0.2);
|
||||
color: var(--owlry-badge-app);
|
||||
}
|
||||
|
||||
.owlry-badge-cmd {
|
||||
background-color: rgba(131, 165, 152, 0.2);
|
||||
color: var(--owlry-badge-cmd);
|
||||
}
|
||||
|
||||
.owlry-badge-dmenu {
|
||||
background-color: rgba(250, 189, 47, 0.2);
|
||||
color: var(--owlry-badge-dmenu);
|
||||
}
|
||||
|
||||
.owlry-badge-uuctl {
|
||||
background-color: rgba(251, 73, 52, 0.2);
|
||||
color: var(--owlry-badge-uuctl);
|
||||
}
|
||||
|
||||
.owlry-filter-button:checked {
|
||||
background-color: rgba(254, 128, 25, 0.2);
|
||||
color: var(--owlry-accent);
|
||||
border-color: rgba(254, 128, 25, 0.4);
|
||||
}
|
||||
|
||||
.owlry-filter-app:checked {
|
||||
background-color: rgba(184, 187, 38, 0.2);
|
||||
color: var(--owlry-badge-app);
|
||||
border-color: rgba(184, 187, 38, 0.4);
|
||||
}
|
||||
|
||||
.owlry-filter-cmd:checked {
|
||||
background-color: rgba(131, 165, 152, 0.2);
|
||||
color: var(--owlry-badge-cmd);
|
||||
border-color: rgba(131, 165, 152, 0.4);
|
||||
}
|
||||
|
||||
.owlry-filter-uuctl:checked {
|
||||
background-color: rgba(251, 73, 52, 0.2);
|
||||
color: var(--owlry-badge-uuctl);
|
||||
border-color: rgba(251, 73, 52, 0.4);
|
||||
}
|
||||
|
||||
scrollbar slider {
|
||||
background-color: rgba(80, 73, 69, 0.5);
|
||||
}
|
||||
|
||||
scrollbar slider:hover {
|
||||
background-color: rgba(102, 92, 84, 0.7);
|
||||
}
|
||||
|
||||
scrollbar slider:active {
|
||||
background-color: var(--owlry-accent);
|
||||
}
|
||||
114
themes/nord.css
Normal file
114
themes/nord.css
Normal file
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Owlry - Nord Theme
|
||||
* An arctic, north-bluish color palette
|
||||
* https://nordtheme.com/
|
||||
*
|
||||
* Usage: Copy to ~/.config/owlry/themes/nord.css
|
||||
* Set theme = "nord" in config.toml
|
||||
*/
|
||||
|
||||
:root {
|
||||
--owlry-bg: #2e3440;
|
||||
--owlry-bg-secondary: #3b4252;
|
||||
--owlry-border: #4c566a;
|
||||
--owlry-text: #eceff4;
|
||||
--owlry-text-secondary: #d8dee9;
|
||||
--owlry-accent: #88c0d0;
|
||||
--owlry-accent-bright: #8fbcbb;
|
||||
--owlry-badge-app: #a3be8c;
|
||||
--owlry-badge-cmd: #81a1c1;
|
||||
--owlry-badge-dmenu: #ebcb8b;
|
||||
--owlry-badge-uuctl: #bf616a;
|
||||
}
|
||||
|
||||
.owlry-main {
|
||||
background-color: rgba(46, 52, 64, 0.95);
|
||||
border: 1px solid rgba(76, 86, 106, 0.6);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4),
|
||||
0 0 0 1px rgba(136, 192, 208, 0.1);
|
||||
}
|
||||
|
||||
.owlry-search {
|
||||
background-color: rgba(59, 66, 82, 0.8);
|
||||
border: 2px solid rgba(76, 86, 106, 0.5);
|
||||
color: var(--owlry-text);
|
||||
caret-color: var(--owlry-accent);
|
||||
}
|
||||
|
||||
.owlry-search:focus {
|
||||
border-color: var(--owlry-accent);
|
||||
box-shadow: 0 0 0 2px rgba(136, 192, 208, 0.2);
|
||||
}
|
||||
|
||||
.owlry-result-row:hover {
|
||||
background-color: rgba(59, 66, 82, 0.6);
|
||||
}
|
||||
|
||||
.owlry-result-row:selected {
|
||||
background-color: rgba(136, 192, 208, 0.15);
|
||||
border-left: 3px solid var(--owlry-accent);
|
||||
}
|
||||
|
||||
.owlry-result-row:selected .owlry-result-name {
|
||||
color: var(--owlry-accent-bright);
|
||||
}
|
||||
|
||||
.owlry-result-row:selected .owlry-result-icon {
|
||||
color: var(--owlry-accent);
|
||||
}
|
||||
|
||||
.owlry-badge-app {
|
||||
background-color: rgba(163, 190, 140, 0.2);
|
||||
color: var(--owlry-badge-app);
|
||||
}
|
||||
|
||||
.owlry-badge-cmd {
|
||||
background-color: rgba(129, 161, 193, 0.2);
|
||||
color: var(--owlry-badge-cmd);
|
||||
}
|
||||
|
||||
.owlry-badge-dmenu {
|
||||
background-color: rgba(235, 203, 139, 0.2);
|
||||
color: var(--owlry-badge-dmenu);
|
||||
}
|
||||
|
||||
.owlry-badge-uuctl {
|
||||
background-color: rgba(191, 97, 106, 0.2);
|
||||
color: var(--owlry-badge-uuctl);
|
||||
}
|
||||
|
||||
.owlry-filter-button:checked {
|
||||
background-color: rgba(136, 192, 208, 0.2);
|
||||
color: var(--owlry-accent);
|
||||
border-color: rgba(136, 192, 208, 0.4);
|
||||
}
|
||||
|
||||
.owlry-filter-app:checked {
|
||||
background-color: rgba(163, 190, 140, 0.2);
|
||||
color: var(--owlry-badge-app);
|
||||
border-color: rgba(163, 190, 140, 0.4);
|
||||
}
|
||||
|
||||
.owlry-filter-cmd:checked {
|
||||
background-color: rgba(129, 161, 193, 0.2);
|
||||
color: var(--owlry-badge-cmd);
|
||||
border-color: rgba(129, 161, 193, 0.4);
|
||||
}
|
||||
|
||||
.owlry-filter-uuctl:checked {
|
||||
background-color: rgba(191, 97, 106, 0.2);
|
||||
color: var(--owlry-badge-uuctl);
|
||||
border-color: rgba(191, 97, 106, 0.4);
|
||||
}
|
||||
|
||||
scrollbar slider {
|
||||
background-color: rgba(76, 86, 106, 0.5);
|
||||
}
|
||||
|
||||
scrollbar slider:hover {
|
||||
background-color: rgba(76, 86, 106, 0.7);
|
||||
}
|
||||
|
||||
scrollbar slider:active {
|
||||
background-color: var(--owlry-accent);
|
||||
}
|
||||
114
themes/one-dark.css
Normal file
114
themes/one-dark.css
Normal file
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Owlry - One Dark Theme
|
||||
* Atom's iconic One Dark theme
|
||||
* https://github.com/atom/atom/tree/master/packages/one-dark-syntax
|
||||
*
|
||||
* Usage: Copy to ~/.config/owlry/themes/one-dark.css
|
||||
* Set theme = "one-dark" in config.toml
|
||||
*/
|
||||
|
||||
:root {
|
||||
--owlry-bg: #282c34;
|
||||
--owlry-bg-secondary: #21252b;
|
||||
--owlry-border: #181a1f;
|
||||
--owlry-text: #abb2bf;
|
||||
--owlry-text-secondary: #5c6370;
|
||||
--owlry-accent: #61afef;
|
||||
--owlry-accent-bright: #c678dd;
|
||||
--owlry-badge-app: #98c379;
|
||||
--owlry-badge-cmd: #61afef;
|
||||
--owlry-badge-dmenu: #e5c07b;
|
||||
--owlry-badge-uuctl: #e06c75;
|
||||
}
|
||||
|
||||
.owlry-main {
|
||||
background-color: rgba(40, 44, 52, 0.95);
|
||||
border: 1px solid rgba(24, 26, 31, 0.6);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5),
|
||||
0 0 0 1px rgba(97, 175, 239, 0.1);
|
||||
}
|
||||
|
||||
.owlry-search {
|
||||
background-color: rgba(33, 37, 43, 0.8);
|
||||
border: 2px solid rgba(24, 26, 31, 0.5);
|
||||
color: var(--owlry-text);
|
||||
caret-color: var(--owlry-accent);
|
||||
}
|
||||
|
||||
.owlry-search:focus {
|
||||
border-color: var(--owlry-accent);
|
||||
box-shadow: 0 0 0 2px rgba(97, 175, 239, 0.2);
|
||||
}
|
||||
|
||||
.owlry-result-row:hover {
|
||||
background-color: rgba(33, 37, 43, 0.6);
|
||||
}
|
||||
|
||||
.owlry-result-row:selected {
|
||||
background-color: rgba(97, 175, 239, 0.15);
|
||||
border-left: 3px solid var(--owlry-accent);
|
||||
}
|
||||
|
||||
.owlry-result-row:selected .owlry-result-name {
|
||||
color: var(--owlry-accent-bright);
|
||||
}
|
||||
|
||||
.owlry-result-row:selected .owlry-result-icon {
|
||||
color: var(--owlry-accent);
|
||||
}
|
||||
|
||||
.owlry-badge-app {
|
||||
background-color: rgba(152, 195, 121, 0.2);
|
||||
color: var(--owlry-badge-app);
|
||||
}
|
||||
|
||||
.owlry-badge-cmd {
|
||||
background-color: rgba(97, 175, 239, 0.2);
|
||||
color: var(--owlry-badge-cmd);
|
||||
}
|
||||
|
||||
.owlry-badge-dmenu {
|
||||
background-color: rgba(229, 192, 123, 0.2);
|
||||
color: var(--owlry-badge-dmenu);
|
||||
}
|
||||
|
||||
.owlry-badge-uuctl {
|
||||
background-color: rgba(224, 108, 117, 0.2);
|
||||
color: var(--owlry-badge-uuctl);
|
||||
}
|
||||
|
||||
.owlry-filter-button:checked {
|
||||
background-color: rgba(97, 175, 239, 0.2);
|
||||
color: var(--owlry-accent);
|
||||
border-color: rgba(97, 175, 239, 0.4);
|
||||
}
|
||||
|
||||
.owlry-filter-app:checked {
|
||||
background-color: rgba(152, 195, 121, 0.2);
|
||||
color: var(--owlry-badge-app);
|
||||
border-color: rgba(152, 195, 121, 0.4);
|
||||
}
|
||||
|
||||
.owlry-filter-cmd:checked {
|
||||
background-color: rgba(97, 175, 239, 0.2);
|
||||
color: var(--owlry-badge-cmd);
|
||||
border-color: rgba(97, 175, 239, 0.4);
|
||||
}
|
||||
|
||||
.owlry-filter-uuctl:checked {
|
||||
background-color: rgba(224, 108, 117, 0.2);
|
||||
color: var(--owlry-badge-uuctl);
|
||||
border-color: rgba(224, 108, 117, 0.4);
|
||||
}
|
||||
|
||||
scrollbar slider {
|
||||
background-color: rgba(92, 99, 112, 0.5);
|
||||
}
|
||||
|
||||
scrollbar slider:hover {
|
||||
background-color: rgba(92, 99, 112, 0.7);
|
||||
}
|
||||
|
||||
scrollbar slider:active {
|
||||
background-color: var(--owlry-accent);
|
||||
}
|
||||
123
themes/owl.css
Normal file
123
themes/owl.css
Normal file
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Owlry - Owl Theme
|
||||
* An owl-inspired dark theme with amber accents
|
||||
*
|
||||
* Color Palette:
|
||||
* - Deep night sky: #1a1b26 (background)
|
||||
* - Twilight: #24283b (secondary bg)
|
||||
* - Owl feathers: #414868 (borders/muted)
|
||||
* - Moon glow: #c0caf5 (primary text)
|
||||
* - Owl eyes (amber): #e0af68 (accent/highlight)
|
||||
* - Forest shadows: #565f89 (secondary text)
|
||||
* - Barn owl cream: #f5e0dc (bright accent)
|
||||
*
|
||||
* Usage: Copy to ~/.config/owlry/themes/owl.css
|
||||
* Set theme = "owl" in config.toml
|
||||
* (Note: "owl" is also built-in, so this file is optional)
|
||||
*/
|
||||
|
||||
:root {
|
||||
--owlry-bg: #1a1b26;
|
||||
--owlry-bg-secondary: #24283b;
|
||||
--owlry-border: #414868;
|
||||
--owlry-text: #c0caf5;
|
||||
--owlry-text-secondary: #565f89;
|
||||
--owlry-accent: #e0af68;
|
||||
--owlry-accent-bright: #f5e0dc;
|
||||
--owlry-badge-app: #7aa2f7;
|
||||
--owlry-badge-cmd: #bb9af7;
|
||||
--owlry-badge-dmenu: #9ece6a;
|
||||
--owlry-badge-uuctl: #e0af68;
|
||||
}
|
||||
|
||||
.owlry-main {
|
||||
background-color: rgba(26, 27, 38, 0.95);
|
||||
border: 1px solid rgba(65, 72, 104, 0.6);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5),
|
||||
0 0 0 1px rgba(224, 175, 104, 0.1);
|
||||
}
|
||||
|
||||
.owlry-search {
|
||||
background-color: rgba(36, 40, 59, 0.8);
|
||||
border: 2px solid rgba(65, 72, 104, 0.5);
|
||||
color: var(--owlry-text);
|
||||
caret-color: var(--owlry-accent);
|
||||
}
|
||||
|
||||
.owlry-search:focus {
|
||||
border-color: var(--owlry-accent);
|
||||
box-shadow: 0 0 0 2px rgba(224, 175, 104, 0.2);
|
||||
}
|
||||
|
||||
.owlry-result-row:hover {
|
||||
background-color: rgba(36, 40, 59, 0.6);
|
||||
}
|
||||
|
||||
.owlry-result-row:selected {
|
||||
background-color: rgba(224, 175, 104, 0.15);
|
||||
border-left: 3px solid var(--owlry-accent);
|
||||
}
|
||||
|
||||
.owlry-result-row:selected .owlry-result-name {
|
||||
color: var(--owlry-accent-bright);
|
||||
}
|
||||
|
||||
.owlry-result-row:selected .owlry-result-icon {
|
||||
color: var(--owlry-accent);
|
||||
}
|
||||
|
||||
.owlry-badge-app {
|
||||
background-color: rgba(122, 162, 247, 0.2);
|
||||
color: var(--owlry-badge-app);
|
||||
}
|
||||
|
||||
.owlry-badge-cmd {
|
||||
background-color: rgba(187, 154, 247, 0.2);
|
||||
color: var(--owlry-badge-cmd);
|
||||
}
|
||||
|
||||
.owlry-badge-dmenu {
|
||||
background-color: rgba(158, 206, 106, 0.2);
|
||||
color: var(--owlry-badge-dmenu);
|
||||
}
|
||||
|
||||
.owlry-badge-uuctl {
|
||||
background-color: rgba(224, 175, 104, 0.2);
|
||||
color: var(--owlry-badge-uuctl);
|
||||
}
|
||||
|
||||
.owlry-filter-button:checked {
|
||||
background-color: rgba(224, 175, 104, 0.2);
|
||||
color: var(--owlry-accent);
|
||||
border-color: rgba(224, 175, 104, 0.4);
|
||||
}
|
||||
|
||||
.owlry-filter-app:checked {
|
||||
background-color: rgba(122, 162, 247, 0.2);
|
||||
color: var(--owlry-badge-app);
|
||||
border-color: rgba(122, 162, 247, 0.4);
|
||||
}
|
||||
|
||||
.owlry-filter-cmd:checked {
|
||||
background-color: rgba(187, 154, 247, 0.2);
|
||||
color: var(--owlry-badge-cmd);
|
||||
border-color: rgba(187, 154, 247, 0.4);
|
||||
}
|
||||
|
||||
.owlry-filter-uuctl:checked {
|
||||
background-color: rgba(224, 175, 104, 0.2);
|
||||
color: var(--owlry-badge-uuctl);
|
||||
border-color: rgba(224, 175, 104, 0.4);
|
||||
}
|
||||
|
||||
scrollbar slider {
|
||||
background-color: rgba(65, 72, 104, 0.5);
|
||||
}
|
||||
|
||||
scrollbar slider:hover {
|
||||
background-color: rgba(86, 95, 137, 0.7);
|
||||
}
|
||||
|
||||
scrollbar slider:active {
|
||||
background-color: var(--owlry-accent);
|
||||
}
|
||||
114
themes/rose-pine.css
Normal file
114
themes/rose-pine.css
Normal file
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Owlry - Rosé Pine Theme
|
||||
* All natural pine, faux fur and a bit of soho vibes
|
||||
* https://rosepinetheme.com/
|
||||
*
|
||||
* Usage: Copy to ~/.config/owlry/themes/rose-pine.css
|
||||
* Set theme = "rose-pine" in config.toml
|
||||
*/
|
||||
|
||||
:root {
|
||||
--owlry-bg: #191724;
|
||||
--owlry-bg-secondary: #1f1d2e;
|
||||
--owlry-border: #26233a;
|
||||
--owlry-text: #e0def4;
|
||||
--owlry-text-secondary: #908caa;
|
||||
--owlry-accent: #c4a7e7;
|
||||
--owlry-accent-bright: #ebbcba;
|
||||
--owlry-badge-app: #9ccfd8;
|
||||
--owlry-badge-cmd: #c4a7e7;
|
||||
--owlry-badge-dmenu: #f6c177;
|
||||
--owlry-badge-uuctl: #eb6f92;
|
||||
}
|
||||
|
||||
.owlry-main {
|
||||
background-color: rgba(25, 23, 36, 0.95);
|
||||
border: 1px solid rgba(38, 35, 58, 0.6);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5),
|
||||
0 0 0 1px rgba(196, 167, 231, 0.1);
|
||||
}
|
||||
|
||||
.owlry-search {
|
||||
background-color: rgba(31, 29, 46, 0.8);
|
||||
border: 2px solid rgba(38, 35, 58, 0.5);
|
||||
color: var(--owlry-text);
|
||||
caret-color: var(--owlry-accent);
|
||||
}
|
||||
|
||||
.owlry-search:focus {
|
||||
border-color: var(--owlry-accent);
|
||||
box-shadow: 0 0 0 2px rgba(196, 167, 231, 0.2);
|
||||
}
|
||||
|
||||
.owlry-result-row:hover {
|
||||
background-color: rgba(31, 29, 46, 0.6);
|
||||
}
|
||||
|
||||
.owlry-result-row:selected {
|
||||
background-color: rgba(196, 167, 231, 0.15);
|
||||
border-left: 3px solid var(--owlry-accent);
|
||||
}
|
||||
|
||||
.owlry-result-row:selected .owlry-result-name {
|
||||
color: var(--owlry-accent-bright);
|
||||
}
|
||||
|
||||
.owlry-result-row:selected .owlry-result-icon {
|
||||
color: var(--owlry-accent);
|
||||
}
|
||||
|
||||
.owlry-badge-app {
|
||||
background-color: rgba(156, 207, 216, 0.2);
|
||||
color: var(--owlry-badge-app);
|
||||
}
|
||||
|
||||
.owlry-badge-cmd {
|
||||
background-color: rgba(196, 167, 231, 0.2);
|
||||
color: var(--owlry-badge-cmd);
|
||||
}
|
||||
|
||||
.owlry-badge-dmenu {
|
||||
background-color: rgba(246, 193, 119, 0.2);
|
||||
color: var(--owlry-badge-dmenu);
|
||||
}
|
||||
|
||||
.owlry-badge-uuctl {
|
||||
background-color: rgba(235, 111, 146, 0.2);
|
||||
color: var(--owlry-badge-uuctl);
|
||||
}
|
||||
|
||||
.owlry-filter-button:checked {
|
||||
background-color: rgba(196, 167, 231, 0.2);
|
||||
color: var(--owlry-accent);
|
||||
border-color: rgba(196, 167, 231, 0.4);
|
||||
}
|
||||
|
||||
.owlry-filter-app:checked {
|
||||
background-color: rgba(156, 207, 216, 0.2);
|
||||
color: var(--owlry-badge-app);
|
||||
border-color: rgba(156, 207, 216, 0.4);
|
||||
}
|
||||
|
||||
.owlry-filter-cmd:checked {
|
||||
background-color: rgba(196, 167, 231, 0.2);
|
||||
color: var(--owlry-badge-cmd);
|
||||
border-color: rgba(196, 167, 231, 0.4);
|
||||
}
|
||||
|
||||
.owlry-filter-uuctl:checked {
|
||||
background-color: rgba(235, 111, 146, 0.2);
|
||||
color: var(--owlry-badge-uuctl);
|
||||
border-color: rgba(235, 111, 146, 0.4);
|
||||
}
|
||||
|
||||
scrollbar slider {
|
||||
background-color: rgba(38, 35, 58, 0.5);
|
||||
}
|
||||
|
||||
scrollbar slider:hover {
|
||||
background-color: rgba(144, 140, 170, 0.5);
|
||||
}
|
||||
|
||||
scrollbar slider:active {
|
||||
background-color: var(--owlry-accent);
|
||||
}
|
||||
114
themes/solarized-dark.css
Normal file
114
themes/solarized-dark.css
Normal file
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Owlry - Solarized Dark Theme
|
||||
* Precision colors for machines and people
|
||||
* https://ethanschoonover.com/solarized/
|
||||
*
|
||||
* Usage: Copy to ~/.config/owlry/themes/solarized-dark.css
|
||||
* Set theme = "solarized-dark" in config.toml
|
||||
*/
|
||||
|
||||
:root {
|
||||
--owlry-bg: #002b36;
|
||||
--owlry-bg-secondary: #073642;
|
||||
--owlry-border: #586e75;
|
||||
--owlry-text: #839496;
|
||||
--owlry-text-secondary: #657b83;
|
||||
--owlry-accent: #268bd2;
|
||||
--owlry-accent-bright: #2aa198;
|
||||
--owlry-badge-app: #859900;
|
||||
--owlry-badge-cmd: #268bd2;
|
||||
--owlry-badge-dmenu: #b58900;
|
||||
--owlry-badge-uuctl: #dc322f;
|
||||
}
|
||||
|
||||
.owlry-main {
|
||||
background-color: rgba(0, 43, 54, 0.95);
|
||||
border: 1px solid rgba(88, 110, 117, 0.6);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5),
|
||||
0 0 0 1px rgba(38, 139, 210, 0.1);
|
||||
}
|
||||
|
||||
.owlry-search {
|
||||
background-color: rgba(7, 54, 66, 0.8);
|
||||
border: 2px solid rgba(88, 110, 117, 0.5);
|
||||
color: var(--owlry-text);
|
||||
caret-color: var(--owlry-accent);
|
||||
}
|
||||
|
||||
.owlry-search:focus {
|
||||
border-color: var(--owlry-accent);
|
||||
box-shadow: 0 0 0 2px rgba(38, 139, 210, 0.2);
|
||||
}
|
||||
|
||||
.owlry-result-row:hover {
|
||||
background-color: rgba(7, 54, 66, 0.6);
|
||||
}
|
||||
|
||||
.owlry-result-row:selected {
|
||||
background-color: rgba(38, 139, 210, 0.15);
|
||||
border-left: 3px solid var(--owlry-accent);
|
||||
}
|
||||
|
||||
.owlry-result-row:selected .owlry-result-name {
|
||||
color: var(--owlry-accent-bright);
|
||||
}
|
||||
|
||||
.owlry-result-row:selected .owlry-result-icon {
|
||||
color: var(--owlry-accent);
|
||||
}
|
||||
|
||||
.owlry-badge-app {
|
||||
background-color: rgba(133, 153, 0, 0.2);
|
||||
color: var(--owlry-badge-app);
|
||||
}
|
||||
|
||||
.owlry-badge-cmd {
|
||||
background-color: rgba(38, 139, 210, 0.2);
|
||||
color: var(--owlry-badge-cmd);
|
||||
}
|
||||
|
||||
.owlry-badge-dmenu {
|
||||
background-color: rgba(181, 137, 0, 0.2);
|
||||
color: var(--owlry-badge-dmenu);
|
||||
}
|
||||
|
||||
.owlry-badge-uuctl {
|
||||
background-color: rgba(220, 50, 47, 0.2);
|
||||
color: var(--owlry-badge-uuctl);
|
||||
}
|
||||
|
||||
.owlry-filter-button:checked {
|
||||
background-color: rgba(38, 139, 210, 0.2);
|
||||
color: var(--owlry-accent);
|
||||
border-color: rgba(38, 139, 210, 0.4);
|
||||
}
|
||||
|
||||
.owlry-filter-app:checked {
|
||||
background-color: rgba(133, 153, 0, 0.2);
|
||||
color: var(--owlry-badge-app);
|
||||
border-color: rgba(133, 153, 0, 0.4);
|
||||
}
|
||||
|
||||
.owlry-filter-cmd:checked {
|
||||
background-color: rgba(38, 139, 210, 0.2);
|
||||
color: var(--owlry-badge-cmd);
|
||||
border-color: rgba(38, 139, 210, 0.4);
|
||||
}
|
||||
|
||||
.owlry-filter-uuctl:checked {
|
||||
background-color: rgba(220, 50, 47, 0.2);
|
||||
color: var(--owlry-badge-uuctl);
|
||||
border-color: rgba(220, 50, 47, 0.4);
|
||||
}
|
||||
|
||||
scrollbar slider {
|
||||
background-color: rgba(88, 110, 117, 0.5);
|
||||
}
|
||||
|
||||
scrollbar slider:hover {
|
||||
background-color: rgba(101, 123, 131, 0.7);
|
||||
}
|
||||
|
||||
scrollbar slider:active {
|
||||
background-color: var(--owlry-accent);
|
||||
}
|
||||
114
themes/tokyo-night.css
Normal file
114
themes/tokyo-night.css
Normal file
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Owlry - Tokyo Night Theme
|
||||
* A clean, dark theme that celebrates the lights of Tokyo at night
|
||||
* https://github.com/enkia/tokyo-night-vscode-theme
|
||||
*
|
||||
* Usage: Copy to ~/.config/owlry/themes/tokyo-night.css
|
||||
* Set theme = "tokyo-night" in config.toml
|
||||
*/
|
||||
|
||||
:root {
|
||||
--owlry-bg: #1a1b26;
|
||||
--owlry-bg-secondary: #24283b;
|
||||
--owlry-border: #414868;
|
||||
--owlry-text: #c0caf5;
|
||||
--owlry-text-secondary: #565f89;
|
||||
--owlry-accent: #7aa2f7;
|
||||
--owlry-accent-bright: #bb9af7;
|
||||
--owlry-badge-app: #9ece6a;
|
||||
--owlry-badge-cmd: #7dcfff;
|
||||
--owlry-badge-dmenu: #e0af68;
|
||||
--owlry-badge-uuctl: #f7768e;
|
||||
}
|
||||
|
||||
.owlry-main {
|
||||
background-color: rgba(26, 27, 38, 0.95);
|
||||
border: 1px solid rgba(65, 72, 104, 0.6);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5),
|
||||
0 0 0 1px rgba(122, 162, 247, 0.1);
|
||||
}
|
||||
|
||||
.owlry-search {
|
||||
background-color: rgba(36, 40, 59, 0.8);
|
||||
border: 2px solid rgba(65, 72, 104, 0.5);
|
||||
color: var(--owlry-text);
|
||||
caret-color: var(--owlry-accent);
|
||||
}
|
||||
|
||||
.owlry-search:focus {
|
||||
border-color: var(--owlry-accent);
|
||||
box-shadow: 0 0 0 2px rgba(122, 162, 247, 0.2);
|
||||
}
|
||||
|
||||
.owlry-result-row:hover {
|
||||
background-color: rgba(36, 40, 59, 0.6);
|
||||
}
|
||||
|
||||
.owlry-result-row:selected {
|
||||
background-color: rgba(122, 162, 247, 0.15);
|
||||
border-left: 3px solid var(--owlry-accent);
|
||||
}
|
||||
|
||||
.owlry-result-row:selected .owlry-result-name {
|
||||
color: var(--owlry-accent-bright);
|
||||
}
|
||||
|
||||
.owlry-result-row:selected .owlry-result-icon {
|
||||
color: var(--owlry-accent);
|
||||
}
|
||||
|
||||
.owlry-badge-app {
|
||||
background-color: rgba(158, 206, 106, 0.2);
|
||||
color: var(--owlry-badge-app);
|
||||
}
|
||||
|
||||
.owlry-badge-cmd {
|
||||
background-color: rgba(125, 207, 255, 0.2);
|
||||
color: var(--owlry-badge-cmd);
|
||||
}
|
||||
|
||||
.owlry-badge-dmenu {
|
||||
background-color: rgba(224, 175, 104, 0.2);
|
||||
color: var(--owlry-badge-dmenu);
|
||||
}
|
||||
|
||||
.owlry-badge-uuctl {
|
||||
background-color: rgba(247, 118, 142, 0.2);
|
||||
color: var(--owlry-badge-uuctl);
|
||||
}
|
||||
|
||||
.owlry-filter-button:checked {
|
||||
background-color: rgba(122, 162, 247, 0.2);
|
||||
color: var(--owlry-accent);
|
||||
border-color: rgba(122, 162, 247, 0.4);
|
||||
}
|
||||
|
||||
.owlry-filter-app:checked {
|
||||
background-color: rgba(158, 206, 106, 0.2);
|
||||
color: var(--owlry-badge-app);
|
||||
border-color: rgba(158, 206, 106, 0.4);
|
||||
}
|
||||
|
||||
.owlry-filter-cmd:checked {
|
||||
background-color: rgba(125, 207, 255, 0.2);
|
||||
color: var(--owlry-badge-cmd);
|
||||
border-color: rgba(125, 207, 255, 0.4);
|
||||
}
|
||||
|
||||
.owlry-filter-uuctl:checked {
|
||||
background-color: rgba(247, 118, 142, 0.2);
|
||||
color: var(--owlry-badge-uuctl);
|
||||
border-color: rgba(247, 118, 142, 0.4);
|
||||
}
|
||||
|
||||
scrollbar slider {
|
||||
background-color: rgba(65, 72, 104, 0.5);
|
||||
}
|
||||
|
||||
scrollbar slider:hover {
|
||||
background-color: rgba(86, 95, 137, 0.7);
|
||||
}
|
||||
|
||||
scrollbar slider:active {
|
||||
background-color: var(--owlry-accent);
|
||||
}
|
||||
Reference in New Issue
Block a user