10 Commits

Author SHA1 Message Date
738fecc6da feat: add calculator provider and frecency tracking
Calculator:
- Type "= expression" or "calc expression" for instant math evaluation
- Supports standard math functions (sqrt, sin, cos, etc.) and constants (pi, e)
- Copies result to clipboard on Enter via wl-copy

Frecency:
- Firefox-style algorithm: score = launch_count * recency_weight
- Boosts frequently and recently launched items in search results
- Persists to ~/.local/share/owlry/frecency.json
- Configurable via providers.frecency and providers.frecency_weight

New config options:
- providers.calculator = true
- providers.frecency = true
- providers.frecency_weight = 0.3

UI updates:
- Added :calc prefix support
- Calculator badge with yellow styling
- Updated hints to show "= calc" syntax

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 17:17:46 +01:00
a1351f05e9 chore: bump version to 0.1.8 2025-12-28 16:42:11 +01:00
7118498773 feat: add popular color scheme themes
Added themes:
- rose-pine
- dracula
- gruvbox-dark
- tokyo-night
- solarized-dark
- one-dark

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 16:42:10 +01:00
3d05e560b1 chore: bump version to 0.1.7 2025-12-28 16:31:19 +01:00
604b902261 feat: add example themes (owl, catppuccin-mocha, nord)
Themes are installed to /usr/share/owlry/themes/ and can be
copied to ~/.config/owlry/themes/ for customization.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 16:31:18 +01:00
bb0b0dfa87 chore: bump version to 0.1.6 2025-12-28 16:22:33 +01:00
fc4dde32eb docs: add example config location to README
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 16:22:32 +01:00
cc1ad7bbb7 chore: bump version to 0.1.5 2025-12-28 16:16:40 +01:00
16ba5b642a docs: add example config and CLAUDE.md
- config.example.toml with all available options documented
- CLAUDE.md with release workflow instructions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 16:16:31 +01:00
1608582cbd fix: detect dmenu mode correctly using fstat
Previously, poll() on /dev/null returned "readable" (EOF),
causing dmenu mode to trigger when launched from keybinds.

Now uses fstat() to check if stdin is a pipe or regular file
before checking for data. Character devices (TTY, /dev/null)
no longer trigger dmenu mode.

Fixes items not showing when launched from window manager keybinds.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 16:07:59 +01:00
27 changed files with 2041 additions and 30 deletions

32
CLAUDE.md Normal file
View 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
View File

@@ -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.1.9"
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"

View File

@@ -1,6 +1,6 @@
[package]
name = "owlry"
version = "0.1.3"
version = "0.1.9"
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

View File

@@ -100,6 +100,13 @@ Example: `:cmd git` searches only PATH commands for "git"
Configuration file: `~/.config/owlry/config.toml`
An example config is installed at `/usr/share/doc/owlry/config.example.toml`:
```bash
mkdir -p ~/.config/owlry
cp /usr/share/doc/owlry/config.example.toml ~/.config/owlry/config.toml
```
```toml
[general]
show_icons = true
@@ -157,15 +164,45 @@ You can override this with `launch_wrapper` in config, or set to empty string `"
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`:

50
config.example.toml Normal file
View File

@@ -0,0 +1,50 @@
# 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

View File

@@ -106,6 +106,11 @@
color: var(--owlry-badge-app, @blue_3);
}
.owlry-badge-calc {
background-color: alpha(var(--owlry-badge-calc, @yellow_3), 0.2);
color: var(--owlry-badge-calc, @yellow_3);
}
.owlry-badge-cmd {
background-color: alpha(var(--owlry-badge-cmd, @purple_3), 0.2);
color: var(--owlry-badge-cmd, @purple_3);
@@ -166,6 +171,12 @@
border-color: alpha(var(--owlry-badge-app, @blue_3), 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-cmd:checked {
background-color: alpha(var(--owlry-badge-cmd, @purple_3), 0.2);
color: var(--owlry-badge-cmd, @purple_3);

View File

@@ -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;
@@ -40,6 +41,7 @@ impl OwlryApp {
let config = Rc::new(RefCell::new(Config::load_or_default()));
let providers = Rc::new(RefCell::new(ProviderManager::new()));
let frecency = Rc::new(RefCell::new(FrecencyStore::load_or_default()));
// Create filter from CLI args and config
let filter = ProviderFilter::new(
@@ -49,7 +51,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();

View File

@@ -35,6 +35,7 @@ pub struct ThemeColors {
pub accent_bright: Option<String>,
// Provider badge colors
pub badge_app: Option<String>,
pub badge_calc: Option<String>,
pub badge_cmd: Option<String>,
pub badge_dmenu: Option<String>,
pub badge_uuctl: Option<String>,
@@ -59,6 +60,23 @@ 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,
}
fn default_true() -> bool {
true
}
fn default_frecency_weight() -> f64 {
0.3
}
/// Detect the best launch wrapper for the current session
@@ -172,6 +190,9 @@ impl Default for Config {
applications: true,
commands: true,
uuctl: true,
calculator: true,
frecency: true,
frecency_weight: 0.3,
},
}
}

223
src/data/frecency.rs Normal file
View 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
View File

@@ -0,0 +1,3 @@
mod frecency;
pub use frecency::FrecencyStore;

View File

@@ -129,6 +129,8 @@ impl ProviderFilter {
let prefixes = [
(":app ", ProviderType::Application),
(":apps ", ProviderType::Application),
(":calc ", ProviderType::Calculator),
(":calculator ", ProviderType::Calculator),
(":cmd ", ProviderType::Command),
(":command ", ProviderType::Command),
(":uuctl ", ProviderType::Uuctl),
@@ -147,6 +149,8 @@ impl ProviderFilter {
let partial_prefixes = [
(":app", ProviderType::Application),
(":apps", ProviderType::Application),
(":calc", ProviderType::Calculator),
(":calculator", ProviderType::Calculator),
(":cmd", ProviderType::Command),
(":command", ProviderType::Command),
(":uuctl", ProviderType::Uuctl),
@@ -172,9 +176,10 @@ 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::Calculator => 1,
ProviderType::Command => 2,
ProviderType::Uuctl => 3,
ProviderType::Dmenu => 4,
});
providers
}
@@ -184,6 +189,7 @@ impl ProviderFilter {
if let Some(prefix) = self.active_prefix {
return match prefix {
ProviderType::Application => "Apps",
ProviderType::Calculator => "Calc",
ProviderType::Command => "Commands",
ProviderType::Uuctl => "uuctl",
ProviderType::Dmenu => "dmenu",
@@ -194,6 +200,7 @@ impl ProviderFilter {
if enabled.len() == 1 {
match enabled[0] {
ProviderType::Application => "Apps",
ProviderType::Calculator => "Calc",
ProviderType::Command => "Commands",
ProviderType::Uuctl => "uuctl",
ProviderType::Dmenu => "dmenu",

View File

@@ -1,6 +1,7 @@
mod app;
mod cli;
mod config;
mod data;
mod filter;
mod providers;
mod theme;

191
src/providers/calculator.rs Normal file
View File

@@ -0,0 +1,191 @@
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();
if let Some(expr) = trimmed.strip_prefix("= ") {
Some(expr.trim())
} else if let Some(expr) = trimmed.strip_prefix("calc ") {
Some(expr.trim())
} else {
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());
}
}

View File

@@ -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
}

View File

@@ -1,9 +1,11 @@
mod application;
mod calculator;
mod command;
mod dmenu;
mod uuctl;
pub use application::ApplicationProvider;
pub use calculator::CalculatorProvider;
pub use command::CommandProvider;
pub use dmenu::DmenuProvider;
pub use uuctl::UuctlProvider;
@@ -12,6 +14,8 @@ 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,6 +32,7 @@ pub struct LaunchItem {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ProviderType {
Application,
Calculator,
Command,
Dmenu,
Uuctl,
@@ -39,10 +44,14 @@ 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),
"calc" | "calculator" => Ok(ProviderType::Calculator),
"cmd" | "command" | "commands" => Ok(ProviderType::Command),
"uuctl" => Ok(ProviderType::Uuctl),
"dmenu" => Ok(ProviderType::Dmenu),
_ => Err(format!("Unknown provider: '{}'. Valid: app, cmd, uuctl", s)),
_ => Err(format!(
"Unknown provider: '{}'. Valid: app, calc, cmd, uuctl",
s
)),
}
}
}
@@ -51,6 +60,7 @@ 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::Calculator => write!(f, "calc"),
ProviderType::Command => write!(f, "cmd"),
ProviderType::Dmenu => write!(f, "dmenu"),
ProviderType::Uuctl => write!(f, "uuctl"),
@@ -70,6 +80,7 @@ pub trait Provider: Send {
/// Manages all providers and handles searching
pub struct ProviderManager {
providers: Vec<Box<dyn Provider>>,
calculator: CalculatorProvider,
matcher: SkimMatcherV2,
}
@@ -77,6 +88,7 @@ impl ProviderManager {
pub fn new() -> Self {
let mut manager = Self {
providers: Vec::new(),
calculator: CalculatorProvider::new(),
matcher: SkimMatcherV2::default(),
};
@@ -206,6 +218,79 @@ 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 first
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));
}
}
// Empty query (after checking calculator) - 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> {

View File

@@ -35,6 +35,9 @@ 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_calc) = config.colors.badge_calc {
css.push_str(&format!(" --owlry-badge-calc: {};\n", badge_calc));
}
if let Some(ref badge_cmd) = config.colors.badge_cmd {
css.push_str(&format!(" --owlry-badge-cmd: {};\n", badge_cmd));
}

View File

@@ -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 :app :cmd :uuctl")
.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,6 +206,7 @@ impl MainWindow {
button.add_css_class("owlry-filter-button");
let css_class = match provider_type {
ProviderType::Application => "owlry-filter-app",
ProviderType::Calculator => "owlry-filter-calc",
ProviderType::Command => "owlry-filter-cmd",
ProviderType::Uuctl => "owlry-filter-uuctl",
ProviderType::Dmenu => "owlry-filter-dmenu",
@@ -221,6 +226,7 @@ impl MainWindow {
.iter()
.map(|p| match p {
ProviderType::Application => "applications",
ProviderType::Calculator => "calculator",
ProviderType::Command => "commands",
ProviderType::Uuctl => "uuctl units",
ProviderType::Dmenu => "options",
@@ -328,7 +334,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 :app :cmd :uuctl");
search_entry.set_placeholder_text(Some(&Self::build_placeholder(&filter.borrow())));
search_entry.set_text(&saved_search);
@@ -341,6 +347,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,6 +408,7 @@ impl MainWindow {
if parsed.prefix.is_some() {
let prefix_name = match parsed.prefix.unwrap() {
ProviderType::Application => "applications",
ProviderType::Calculator => "calculator",
ProviderType::Command => "commands",
ProviderType::Uuctl => "uuctl units",
ProviderType::Dmenu => "options",
@@ -409,13 +417,27 @@ impl MainWindow {
.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 +459,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 +493,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 +670,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 +699,7 @@ impl MainWindow {
is_active,
);
} else {
Self::launch_item(item, &config.borrow());
Self::launch_item(item, &config.borrow(), &frecency);
window.close();
}
}
@@ -743,14 +767,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 +805,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 {

View File

@@ -32,6 +32,7 @@ impl ResultRow {
// Default icon based on provider type
let default_icon = match item.provider {
crate::providers::ProviderType::Application => "application-x-executable",
crate::providers::ProviderType::Calculator => "accessories-calculator",
crate::providers::ProviderType::Command => "utilities-terminal",
crate::providers::ProviderType::Dmenu => "view-list-symbolic",
crate::providers::ProviderType::Uuctl => "system-run",

114
themes/catppuccin-mocha.css Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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);
}