Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a750ef8559 | |||
| 7cbebd324f | |||
| 5519381d8c | |||
| 38025279f9 | |||
| 405b598b9b | |||
| d086995399 | |||
| 7ca8a1f443 | |||
| 2a2a22f72c | |||
| 0eccdc5883 | |||
| 3f7a8950eb | |||
| b38bf082e1 | |||
| 617dbbce3e | |||
| 4ff054afe0 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
||||
/target
|
||||
CLAUDE.md
|
||||
|
||||
32
CLAUDE.md
32
CLAUDE.md
@@ -1,32 +0,0 @@
|
||||
# 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
|
||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -971,7 +971,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "owlry"
|
||||
version = "0.3.2"
|
||||
version = "0.3.7"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"clap",
|
||||
|
||||
13
Cargo.toml
13
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "owlry"
|
||||
version = "0.3.2"
|
||||
version = "0.3.7"
|
||||
edition = "2024"
|
||||
rust-version = "1.90"
|
||||
description = "A lightweight, owl-themed application launcher for Wayland"
|
||||
@@ -55,6 +55,11 @@ serde_json = "1"
|
||||
# Date/time for frecency calculations
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
# Enable verbose debug logging (for development/testing builds)
|
||||
dev-logging = []
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
@@ -65,3 +70,9 @@ opt-level = "z" # Optimize for size
|
||||
[profile.dev]
|
||||
opt-level = 0
|
||||
debug = true
|
||||
|
||||
# For installing a testable build: cargo install --path . --profile dev-install --features dev-logging
|
||||
[profile.dev-install]
|
||||
inherits = "release"
|
||||
strip = false
|
||||
debug = 1 # Basic debug info for stack traces
|
||||
|
||||
343
README.md
343
README.md
@@ -10,32 +10,30 @@ A lightweight, owl-themed application launcher for Wayland, built with GTK4 and
|
||||
|
||||
## Features
|
||||
|
||||
- **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`, `: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
|
||||
- **Wayland native** - Uses Layer Shell for proper overlay behavior
|
||||
- **Provider-based architecture** — Search applications, commands, system actions, SSH hosts, clipboard history, bookmarks, emoji, and more
|
||||
- **Fuzzy search with tags** — Fast matching across names, descriptions, and category tags
|
||||
- **Configurable tabs** — Customize header tabs and keyboard shortcuts
|
||||
- **Filter prefixes** — Scope searches with `:app`, `:cmd`, `:tag:development`, etc.
|
||||
- **Calculator** — Quick math with `= 5+3` or `calc sin(pi/2)`
|
||||
- **Web search** — Search the web with `? query`
|
||||
- **File search** — Find files with `/ filename` (requires `fd` or `locate`)
|
||||
- **Frecency ranking** — Frequently/recently used items rank higher
|
||||
- **GTK4 theming** — System theme by default, with 9 built-in themes
|
||||
- **Wayland native** — Uses Layer Shell for proper overlay behavior
|
||||
|
||||
## Installation
|
||||
|
||||
### Arch Linux (AUR)
|
||||
|
||||
```bash
|
||||
# Using yay
|
||||
yay -S owlry
|
||||
|
||||
# Using paru
|
||||
# or
|
||||
paru -S owlry
|
||||
```
|
||||
|
||||
### Build from source
|
||||
|
||||
#### Dependencies
|
||||
### Build from Source
|
||||
|
||||
**Dependencies:**
|
||||
```bash
|
||||
# Arch Linux
|
||||
sudo pacman -S gtk4 gtk4-layer-shell
|
||||
@@ -47,42 +45,31 @@ sudo apt install libgtk-4-dev libgtk4-layer-shell-dev
|
||||
sudo dnf install gtk4-devel gtk4-layer-shell-devel
|
||||
```
|
||||
|
||||
#### Optional dependencies
|
||||
|
||||
**Optional dependencies:**
|
||||
```bash
|
||||
# For clipboard history
|
||||
# Clipboard history
|
||||
sudo pacman -S cliphist wl-clipboard
|
||||
|
||||
# For file search
|
||||
sudo pacman -S fd # or: mlocate
|
||||
# File search (choose one)
|
||||
sudo pacman -S fd # recommended
|
||||
sudo pacman -S mlocate # alternative
|
||||
```
|
||||
|
||||
#### Build
|
||||
|
||||
Requires Rust 1.90 or later.
|
||||
|
||||
**Build (requires Rust 1.90+):**
|
||||
```bash
|
||||
git clone https://somegit.dev/Owlibou/owlry.git
|
||||
cd owlry
|
||||
cargo build --release
|
||||
# Binary: target/release/owlry
|
||||
```
|
||||
|
||||
The binary will be at `target/release/owlry`.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Launch with default settings (GTK theme, all providers)
|
||||
owlry
|
||||
|
||||
# Launch with only applications
|
||||
owlry --mode app
|
||||
|
||||
# Launch with specific providers
|
||||
owlry --providers app,cmd
|
||||
|
||||
# Show help
|
||||
owlry --help
|
||||
owlry # Launch with defaults
|
||||
owlry --mode app # Applications only
|
||||
owlry --providers app,cmd # Specific providers
|
||||
owlry --help # Show all options
|
||||
```
|
||||
|
||||
### Keyboard Shortcuts
|
||||
@@ -91,135 +78,84 @@ owlry --help
|
||||
|-----|--------|
|
||||
| `Enter` | Launch selected item |
|
||||
| `Escape` | Close launcher / exit submenu |
|
||||
| `Up` / `Down` | Navigate results |
|
||||
| `Tab` | Cycle filter modes |
|
||||
| `Shift+Tab` | Cycle filter modes (reverse) |
|
||||
| `Ctrl+1` | Toggle Applications filter |
|
||||
| `Ctrl+2` | Toggle Commands filter |
|
||||
| `Ctrl+3` | Toggle systemd filter |
|
||||
| `↑` / `↓` | Navigate results |
|
||||
| `Tab` | Cycle filter tabs |
|
||||
| `Shift+Tab` | Cycle filter tabs (reverse) |
|
||||
| `Ctrl+1..9` | Toggle tab by position |
|
||||
|
||||
### Search Prefixes
|
||||
|
||||
Filter results by provider using prefixes:
|
||||
|
||||
| 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` |
|
||||
| `:clip` | Clipboard | `:clip password` |
|
||||
| `:bm` | Bookmarks | `:bm github` |
|
||||
| `:emoji` | Emoji | `:emoji heart` |
|
||||
| `:script` | Scripts | `:script backup` |
|
||||
| `:file` | Files | `:file config` |
|
||||
| `:calc` | Calculator | `:calc sqrt(16)` |
|
||||
| `:web` | Web search | `:web rust docs` |
|
||||
| `:uuctl` | systemd services | `:uuctl docker` |
|
||||
| `:uuctl` | systemd | `:uuctl docker` |
|
||||
| `:tag:X` | Filter by tag | `:tag:development` |
|
||||
|
||||
### Trigger Prefixes
|
||||
|
||||
Some providers can be triggered directly without filter mode:
|
||||
|
||||
| Trigger | Provider | Example |
|
||||
|---------|----------|---------|
|
||||
| `=` | Calculator | `= 5+3` or `=5*2` |
|
||||
| `=` | Calculator | `= 5+3` |
|
||||
| `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
|
||||
## File Locations
|
||||
|
||||
### Applications
|
||||
Searches `.desktop` files from standard XDG directories.
|
||||
Owlry follows the [XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/latest/):
|
||||
|
||||
### 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)
|
||||
| Path | Purpose |
|
||||
|------|---------|
|
||||
| `~/.config/owlry/config.toml` | Main configuration |
|
||||
| `~/.config/owlry/themes/*.css` | Custom themes |
|
||||
| `~/.config/owlry/style.css` | CSS overrides |
|
||||
| `~/.local/share/owlry/scripts/` | User scripts |
|
||||
| `~/.local/share/owlry/frecency.json` | Usage history |
|
||||
|
||||
## Configuration
|
||||
|
||||
Configuration file: `~/.config/owlry/config.toml`
|
||||
|
||||
Copy the example files:
|
||||
```bash
|
||||
# Config
|
||||
mkdir -p ~/.config/owlry
|
||||
cp /usr/share/doc/owlry/config.example.toml ~/.config/owlry/config.toml
|
||||
|
||||
# Optional: CSS overrides
|
||||
cp /usr/share/doc/owlry/style.example.css ~/.config/owlry/style.css
|
||||
|
||||
# Optional: Example script
|
||||
mkdir -p ~/.local/share/owlry/scripts
|
||||
cp /usr/share/doc/owlry/scripts/example.sh ~/.local/share/owlry/scripts/
|
||||
```
|
||||
|
||||
### Example Configuration
|
||||
|
||||
```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
|
||||
tabs = ["app", "cmd", "uuctl"] # Header tabs (Ctrl+1, Ctrl+2, etc.)
|
||||
# terminal_command = "kitty" # Auto-detected
|
||||
# launch_wrapper = "uwsm app --" # Auto-detected
|
||||
|
||||
[appearance]
|
||||
width = 600
|
||||
height = 400
|
||||
font_size = 14
|
||||
border_radius = 12
|
||||
# theme = "owl" # Optional: "owl" or custom theme name
|
||||
# theme = "owl" # Or: catppuccin-mocha, nord, dracula, etc.
|
||||
|
||||
[providers]
|
||||
applications = true
|
||||
@@ -227,7 +163,7 @@ commands = true
|
||||
uuctl = true
|
||||
calculator = true
|
||||
websearch = true
|
||||
search_engine = "duckduckgo" # google, bing, brave, ecosia, startpage, searxng
|
||||
search_engine = "duckduckgo"
|
||||
system = true
|
||||
ssh = true
|
||||
clipboard = true
|
||||
@@ -236,72 +172,83 @@ emoji = true
|
||||
scripts = true
|
||||
files = true
|
||||
frecency = true
|
||||
frecency_weight = 0.3 # 0.0 = disabled, 1.0 = strong boost
|
||||
frecency_weight = 0.3
|
||||
```
|
||||
|
||||
### Default Values
|
||||
### Tab Configuration
|
||||
|
||||
| Setting | Default |
|
||||
|---------|---------|
|
||||
| `show_icons` | `true` |
|
||||
| `max_results` | `10` |
|
||||
| `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` |
|
||||
| `border_radius` | `12` |
|
||||
| `theme` | None (GTK default) |
|
||||
Customize which providers appear as header tabs:
|
||||
|
||||
### Launch Wrapper
|
||||
```toml
|
||||
[general]
|
||||
# Available: app, cmd, uuctl, bookmark, calc, clip, dmenu,
|
||||
# emoji, file, script, ssh, sys, web
|
||||
tabs = ["app", "cmd", "ssh", "sys"]
|
||||
```
|
||||
|
||||
When running in uwsm-managed or Hyprland sessions, owlry auto-detects and uses the appropriate launch wrapper:
|
||||
Keyboard shortcuts `Ctrl+1` through `Ctrl+9` map to tab positions.
|
||||
|
||||
| Session | Wrapper | Purpose |
|
||||
|---------|---------|---------|
|
||||
| uwsm | `uwsm app --` | Proper systemd scope and session management |
|
||||
| Hyprland | `hyprctl dispatch exec --` | Native Hyprland window management |
|
||||
| Other | None (direct `sh -c`) | Standard shell execution |
|
||||
## Providers
|
||||
|
||||
| Provider | Description | Trigger |
|
||||
|----------|-------------|---------|
|
||||
| **Applications** | `.desktop` files from XDG directories | `:app` |
|
||||
| **Commands** | Executables in `$PATH` | `:cmd` |
|
||||
| **System** | Shutdown, reboot, suspend, lock, BIOS | `:sys` |
|
||||
| **SSH** | Hosts from `~/.ssh/config` | `:ssh` |
|
||||
| **Clipboard** | History via cliphist | `:clip` |
|
||||
| **Bookmarks** | Chrome, Brave, Edge, Vivaldi | `:bm` |
|
||||
| **Emoji** | 300+ searchable emoji | `:emoji` |
|
||||
| **Scripts** | User scripts | `:script` |
|
||||
| **Calculator** | Math expressions | `=` or `:calc` |
|
||||
| **Web Search** | Configurable engine | `?` or `:web` |
|
||||
| **Files** | fd/locate search | `/` or `:file` |
|
||||
| **systemd** | User services with actions | `:uuctl` |
|
||||
|
||||
### Tags
|
||||
|
||||
Items are tagged for better search:
|
||||
- **Applications**: Categories from `.desktop` files (development, utility, etc.)
|
||||
- **System**: `power`, `system`
|
||||
- **SSH**: `ssh`
|
||||
- **Scripts**: `script`
|
||||
- **systemd**: `systemd`, `service`
|
||||
|
||||
Filter by tag with `:tag:tagname`:
|
||||
```
|
||||
:tag:development # Show development apps
|
||||
:tag:utility vim # Search utilities for "vim"
|
||||
```
|
||||
|
||||
### Scripts
|
||||
|
||||
Create executable scripts in `~/.local/share/owlry/scripts/`:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.local/share/owlry/scripts
|
||||
cat > ~/.local/share/owlry/scripts/backup.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
rsync -av ~/Documents /backup/
|
||||
notify-send "Backup complete"
|
||||
EOF
|
||||
chmod +x ~/.local/share/owlry/scripts/backup.sh
|
||||
```
|
||||
|
||||
## Theming
|
||||
|
||||
### GTK Theme (Default)
|
||||
|
||||
By default, Owlry inherits colors from your system GTK4 theme (Adwaita, Breeze, etc.).
|
||||
|
||||
### Built-in Themes
|
||||
|
||||
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:
|
||||
| `owl` | Dark theme with amber accents |
|
||||
| `catppuccin-mocha` | Soothing pastel |
|
||||
| `nord` | Arctic blue palette |
|
||||
| `rose-pine` | Natural pine vibes |
|
||||
| `dracula` | Dark vampire theme |
|
||||
| `gruvbox-dark` | Retro groove |
|
||||
| `tokyo-night` | Tokyo city lights |
|
||||
| `solarized-dark` | Precision colors |
|
||||
| `one-dark` | Atom's One Dark |
|
||||
|
||||
```toml
|
||||
[appearance]
|
||||
@@ -310,7 +257,7 @@ theme = "catppuccin-mocha"
|
||||
|
||||
### Custom Theme
|
||||
|
||||
Create a custom theme file at `~/.config/owlry/themes/mytheme.css`:
|
||||
Create `~/.config/owlry/themes/mytheme.css`:
|
||||
|
||||
```css
|
||||
:root {
|
||||
@@ -324,7 +271,24 @@ Create a custom theme file at `~/.config/owlry/themes/mytheme.css`:
|
||||
}
|
||||
```
|
||||
|
||||
### CSS Variables Reference
|
||||
### CSS Overrides
|
||||
|
||||
For tweaks without a full theme, create `~/.config/owlry/style.css`:
|
||||
|
||||
```css
|
||||
/* Larger search input */
|
||||
.owlry-search {
|
||||
font-size: 18px;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
/* Hide tag badges */
|
||||
.owlry-tag-badge {
|
||||
display: none;
|
||||
}
|
||||
```
|
||||
|
||||
### CSS Variables
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
@@ -333,22 +297,17 @@ Create a custom theme file at `~/.config/owlry/themes/mytheme.css`:
|
||||
| `--owlry-border` | Border color |
|
||||
| `--owlry-text` | Primary text |
|
||||
| `--owlry-text-secondary` | Muted text |
|
||||
| `--owlry-accent` | Accent/highlight color |
|
||||
| `--owlry-accent` | Accent color |
|
||||
| `--owlry-accent-bright` | Bright accent |
|
||||
| `--owlry-font-size` | Base font size |
|
||||
| `--owlry-border-radius` | Border radius |
|
||||
| `--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.
|
||||
| `--owlry-border-radius` | Corner radius |
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the GNU General Public License v3.0 or later - see [LICENSE](LICENSE) for details.
|
||||
GNU General Public License v3.0 — see [LICENSE](LICENSE).
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
- [GTK4](https://gtk.org/) - UI toolkit
|
||||
- [gtk4-layer-shell](https://github.com/wmww/gtk4-layer-shell) - Wayland Layer Shell bindings
|
||||
- [fuzzy-matcher](https://crates.io/crates/fuzzy-matcher) - Fuzzy search algorithm
|
||||
- [GTK4](https://gtk.org/) — UI toolkit
|
||||
- [gtk4-layer-shell](https://github.com/wmww/gtk4-layer-shell) — Wayland Layer Shell
|
||||
- [fuzzy-matcher](https://crates.io/crates/fuzzy-matcher) — Fuzzy search
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
# 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
|
||||
115
data/config.example.toml
Normal file
115
data/config.example.toml
Normal file
@@ -0,0 +1,115 @@
|
||||
# Owlry Configuration
|
||||
# Copy to: ~/.config/owlry/config.toml
|
||||
#
|
||||
# File Locations (XDG Base Directory compliant):
|
||||
# ┌─────────────────────────────────────────────────────────────────────┐
|
||||
# │ Config: ~/.config/owlry/config.toml Main configuration │
|
||||
# │ Themes: ~/.config/owlry/themes/*.css Custom theme files │
|
||||
# │ Style: ~/.config/owlry/style.css CSS overrides │
|
||||
# │ Scripts: ~/.local/share/owlry/scripts/ Executable scripts │
|
||||
# │ Data: ~/.local/share/owlry/frecency.json Usage history │
|
||||
# └─────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
# GENERAL
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
[general]
|
||||
show_icons = true
|
||||
max_results = 10
|
||||
|
||||
# Terminal emulator (auto-detected if not set)
|
||||
terminal_command = "kitty"
|
||||
|
||||
# Launch wrapper for app execution (auto-detected for uwsm/Hyprland)
|
||||
# Examples: "uwsm app --", "hyprctl dispatch exec --", ""
|
||||
# launch_wrapper = "uwsm app --"
|
||||
|
||||
# Header tabs - providers shown as toggle buttons (Ctrl+1, Ctrl+2, etc.)
|
||||
# Values: app, cmd, uuctl, bookmark, calc, clip, dmenu, emoji, file, script, ssh, sys, web
|
||||
tabs = ["app", "cmd", "uuctl"]
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
# APPEARANCE
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
[appearance]
|
||||
width = 600
|
||||
height = 400
|
||||
font_size = 14
|
||||
border_radius = 12
|
||||
|
||||
# Theme name - loads ~/.config/owlry/themes/{name}.css
|
||||
# Built-in: owl, catppuccin-mocha, dracula, gruvbox-dark, nord,
|
||||
# one-dark, rose-pine, solarized-dark, tokyo-night
|
||||
# Or leave unset for GTK default
|
||||
# theme = "owl"
|
||||
|
||||
# Color overrides (applied on top of theme)
|
||||
# [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
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
[providers]
|
||||
# Core providers (appear in main search)
|
||||
applications = true # .desktop applications
|
||||
commands = true # Executables from $PATH
|
||||
uuctl = true # systemd --user units
|
||||
|
||||
# Frecency - boost frequently/recently used items
|
||||
# Data: ~/.local/share/owlry/frecency.json
|
||||
frecency = true
|
||||
frecency_weight = 0.3 # 0.0 = disabled, 1.0 = strong boost
|
||||
|
||||
# ───────────────────────────────────────────────────────────────────────
|
||||
# Trigger Providers (activated by prefix)
|
||||
# ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
# Calculator: "= 5+3" or "calc 5+3" or ":calc"
|
||||
calculator = true
|
||||
|
||||
# Web search: "? query" or "web query" or ":web"
|
||||
websearch = true
|
||||
search_engine = "duckduckgo"
|
||||
# Options: google, duckduckgo, bing, startpage, searxng, brave, ecosia
|
||||
# Custom: "https://search.example.com/?q={query}"
|
||||
|
||||
# File search: "/ pattern" or "find pattern" or ":file"
|
||||
# Requires: fd or locate
|
||||
files = true
|
||||
|
||||
# ───────────────────────────────────────────────────────────────────────
|
||||
# Prefix Providers (use :prefix to search)
|
||||
# ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
# System: :sys or :power - shutdown, reboot, lock, suspend, hibernate, logout
|
||||
system = true
|
||||
|
||||
# SSH: :ssh - connections from ~/.ssh/config
|
||||
ssh = true
|
||||
|
||||
# Clipboard: :clip - history (requires cliphist)
|
||||
clipboard = true
|
||||
|
||||
# Bookmarks: :bm - browser bookmarks (Chrome, Chromium, Brave, Edge, Vivaldi)
|
||||
bookmarks = true
|
||||
|
||||
# Emoji: :emoji - picker (copies to clipboard)
|
||||
emoji = true
|
||||
|
||||
# Scripts: :script - executables from ~/.local/share/owlry/scripts/
|
||||
scripts = true
|
||||
24
data/scripts/example.sh
Normal file
24
data/scripts/example.sh
Normal file
@@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
# Example Owlry Script
|
||||
# Copy to: ~/.local/share/owlry/scripts/
|
||||
#
|
||||
# Scripts in the scripts directory appear in Owlry search results.
|
||||
# They are executed when selected.
|
||||
#
|
||||
# Naming convention:
|
||||
# The filename (without extension) becomes the display name.
|
||||
# Example: "system-update.sh" shows as "Script: system-update"
|
||||
#
|
||||
# Tips:
|
||||
# - Make scripts executable: chmod +x script.sh
|
||||
# - Use descriptive names for easy searching
|
||||
# - Scripts can launch GUI apps, run terminal commands, etc.
|
||||
|
||||
# Example: Show a notification
|
||||
notify-send "Owlry" "Hello from example script!"
|
||||
|
||||
# Example: Open a URL
|
||||
# xdg-open "https://example.com"
|
||||
|
||||
# Example: Run a terminal command (set terminal: true in owlry if needed)
|
||||
# echo "Script executed at $(date)"
|
||||
73
data/style.example.css
Normal file
73
data/style.example.css
Normal file
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Owlry Custom Style Overrides
|
||||
* Copy to: ~/.config/owlry/style.css
|
||||
*
|
||||
* This file is loaded AFTER themes, allowing you to override
|
||||
* specific styles without creating a full theme.
|
||||
*
|
||||
* Available CSS classes:
|
||||
* .owlry-window - Main window container
|
||||
* .owlry-main - Main content area
|
||||
* .owlry-header - Header with mode and tabs
|
||||
* .owlry-search - Search input field
|
||||
* .owlry-results - Results list container
|
||||
* .owlry-result-row - Individual result row
|
||||
* .owlry-result-name - Result item name
|
||||
* .owlry-result-description - Result description text
|
||||
* .owlry-result-icon - Result icon
|
||||
* .owlry-tag-badge - Tag badges on results
|
||||
* .owlry-badge-* - Provider badges (app, cmd, uuctl, etc.)
|
||||
* .owlry-filter-button - Tab filter buttons
|
||||
* .owlry-filter-* - Provider-specific filter buttons
|
||||
* .owlry-mode-indicator - Current mode label
|
||||
* .owlry-hints - Bottom hints bar
|
||||
*
|
||||
* CSS Variables (set in themes or override here):
|
||||
* --owlry-bg - Main background color
|
||||
* --owlry-bg-secondary - Secondary background
|
||||
* --owlry-border - Border color
|
||||
* --owlry-text - Primary text color
|
||||
* --owlry-text-secondary - Secondary text color
|
||||
* --owlry-accent - Accent/highlight color
|
||||
* --owlry-accent-bright - Bright accent color
|
||||
* --owlry-font-size - Base font size (default: 14px)
|
||||
* --owlry-border-radius - Border radius (default: 12px)
|
||||
*/
|
||||
|
||||
/* Example: Make the window slightly larger */
|
||||
/*
|
||||
.owlry-main {
|
||||
padding: 20px;
|
||||
}
|
||||
*/
|
||||
|
||||
/* Example: Custom search field styling */
|
||||
/*
|
||||
.owlry-search {
|
||||
font-size: 18px;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
*/
|
||||
|
||||
/* Example: Highlight selected row differently */
|
||||
/*
|
||||
.owlry-result-row:selected {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
border-left: 4px solid var(--owlry-accent);
|
||||
}
|
||||
*/
|
||||
|
||||
/* Example: Hide tag badges */
|
||||
/*
|
||||
.owlry-tag-badge {
|
||||
display: none;
|
||||
}
|
||||
*/
|
||||
|
||||
/* Example: Custom scrollbar */
|
||||
/*
|
||||
scrollbar slider {
|
||||
background-color: rgba(128, 128, 128, 0.3);
|
||||
border-radius: 4px;
|
||||
}
|
||||
*/
|
||||
13
src/app.rs
13
src/app.rs
@@ -2,6 +2,7 @@ use crate::cli::CliArgs;
|
||||
use crate::config::Config;
|
||||
use crate::data::FrecencyStore;
|
||||
use crate::filter::ProviderFilter;
|
||||
use crate::paths;
|
||||
use crate::providers::ProviderManager;
|
||||
use crate::theme;
|
||||
use crate::ui::MainWindow;
|
||||
@@ -81,7 +82,7 @@ impl OwlryApp {
|
||||
|
||||
// 1. Load base structural CSS (always applied)
|
||||
let base_provider = CssProvider::new();
|
||||
base_provider.load_from_string(include_str!("../resources/base.css"));
|
||||
base_provider.load_from_string(include_str!("resources/base.css"));
|
||||
gtk4::style_context_add_provider_for_display(
|
||||
&display,
|
||||
&base_provider,
|
||||
@@ -94,14 +95,12 @@ impl OwlryApp {
|
||||
let theme_provider = CssProvider::new();
|
||||
match theme_name.as_str() {
|
||||
"owl" => {
|
||||
theme_provider.load_from_string(include_str!("../resources/owl-theme.css"));
|
||||
theme_provider.load_from_string(include_str!("resources/owl-theme.css"));
|
||||
debug!("Loaded built-in owl theme");
|
||||
}
|
||||
_ => {
|
||||
// Check for custom theme in ~/.config/owlry/themes/{name}.css
|
||||
if let Some(theme_path) = dirs::config_dir()
|
||||
.map(|p| p.join("owlry").join("themes").join(format!("{}.css", theme_name)))
|
||||
{
|
||||
// Check for custom theme in $XDG_CONFIG_HOME/owlry/themes/{name}.css
|
||||
if let Some(theme_path) = paths::theme_file(theme_name) {
|
||||
if theme_path.exists() {
|
||||
theme_provider.load_from_path(&theme_path);
|
||||
debug!("Loaded custom theme from {:?}", theme_path);
|
||||
@@ -119,7 +118,7 @@ impl OwlryApp {
|
||||
}
|
||||
|
||||
// 3. Load user's custom stylesheet if exists
|
||||
if let Some(custom_path) = dirs::config_dir().map(|p| p.join("owlry").join("style.css")) {
|
||||
if let Some(custom_path) = paths::custom_style_file() {
|
||||
if custom_path.exists() {
|
||||
let custom_provider = CssProvider::new();
|
||||
custom_provider.load_from_path(&custom_path);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use log::{debug, info, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use log::{info, warn, debug};
|
||||
|
||||
use crate::paths;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
@@ -20,6 +22,18 @@ pub struct GeneralConfig {
|
||||
/// If None or empty, launches directly via sh -c
|
||||
#[serde(default)]
|
||||
pub launch_wrapper: Option<String>,
|
||||
/// Provider tabs shown in the header bar.
|
||||
/// Valid values: app, cmd, uuctl, bookmark, calc, clip, dmenu, emoji, file, script, ssh, sys, web
|
||||
#[serde(default = "default_tabs")]
|
||||
pub tabs: Vec<String>,
|
||||
}
|
||||
|
||||
fn default_tabs() -> Vec<String> {
|
||||
vec![
|
||||
"app".to_string(),
|
||||
"cmd".to_string(),
|
||||
"uuctl".to_string(),
|
||||
]
|
||||
}
|
||||
|
||||
/// User-customizable theme colors
|
||||
@@ -218,6 +232,7 @@ impl Default for Config {
|
||||
max_results: 10,
|
||||
terminal_command: terminal,
|
||||
launch_wrapper: detect_launch_wrapper(),
|
||||
tabs: default_tabs(),
|
||||
},
|
||||
appearance: AppearanceConfig {
|
||||
width: 600,
|
||||
@@ -250,7 +265,7 @@ impl Default for Config {
|
||||
|
||||
impl Config {
|
||||
pub fn config_path() -> Option<PathBuf> {
|
||||
dirs::config_dir().map(|p| p.join("owlry").join("config.toml"))
|
||||
paths::config_file()
|
||||
}
|
||||
|
||||
pub fn load_or_default() -> Self {
|
||||
@@ -289,9 +304,7 @@ impl Config {
|
||||
pub fn save(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let path = Self::config_path().ok_or("Could not determine config path")?;
|
||||
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
paths::ensure_parent_dir(&path)?;
|
||||
|
||||
let content = toml::to_string_pretty(self)?;
|
||||
std::fs::write(&path, content)?;
|
||||
|
||||
@@ -4,6 +4,8 @@ use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::paths;
|
||||
|
||||
/// A single frecency entry tracking launch count and recency
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FrecencyEntry {
|
||||
@@ -56,10 +58,7 @@ impl FrecencyStore {
|
||||
|
||||
/// 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")
|
||||
paths::frecency_file().unwrap_or_else(|| PathBuf::from("frecency.json"))
|
||||
}
|
||||
|
||||
/// Load frecency data from a file
|
||||
@@ -85,10 +84,7 @@ impl FrecencyStore {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Ensure directory exists
|
||||
if let Some(parent) = self.path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
paths::ensure_parent_dir(&self.path)?;
|
||||
|
||||
let content = serde_json::to_string_pretty(&self.data)?;
|
||||
std::fs::write(&self.path, content)?;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[cfg(feature = "dev-logging")]
|
||||
use log::debug;
|
||||
|
||||
use crate::config::ProvidersConfig;
|
||||
use crate::providers::ProviderType;
|
||||
|
||||
@@ -14,6 +17,7 @@ pub struct ProviderFilter {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ParsedQuery {
|
||||
pub prefix: Option<ProviderType>,
|
||||
pub tag_filter: Option<String>,
|
||||
pub query: String,
|
||||
}
|
||||
|
||||
@@ -69,10 +73,15 @@ impl ProviderFilter {
|
||||
set
|
||||
};
|
||||
|
||||
Self {
|
||||
let filter = Self {
|
||||
enabled,
|
||||
active_prefix: None,
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(feature = "dev-logging")]
|
||||
debug!("[Filter] Created with enabled providers: {:?}", filter.enabled);
|
||||
|
||||
filter
|
||||
}
|
||||
|
||||
/// Default filter: apps only
|
||||
@@ -92,8 +101,12 @@ impl ProviderFilter {
|
||||
if self.enabled.is_empty() {
|
||||
self.enabled.insert(ProviderType::Application);
|
||||
}
|
||||
#[cfg(feature = "dev-logging")]
|
||||
debug!("[Filter] Toggled OFF {:?}, enabled: {:?}", provider, self.enabled);
|
||||
} else {
|
||||
self.enabled.insert(provider);
|
||||
#[cfg(feature = "dev-logging")]
|
||||
debug!("[Filter] Toggled ON {:?}, enabled: {:?}", provider, self.enabled);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,6 +131,10 @@ impl ProviderFilter {
|
||||
|
||||
/// Set prefix mode (from :app, :cmd, etc.)
|
||||
pub fn set_prefix(&mut self, prefix: Option<ProviderType>) {
|
||||
#[cfg(feature = "dev-logging")]
|
||||
if self.active_prefix != prefix {
|
||||
debug!("[Filter] Prefix changed: {:?} -> {:?}", self.active_prefix, prefix);
|
||||
}
|
||||
self.active_prefix = prefix;
|
||||
}
|
||||
|
||||
@@ -145,6 +162,30 @@ impl ProviderFilter {
|
||||
pub fn parse_query(query: &str) -> ParsedQuery {
|
||||
let trimmed = query.trim_start();
|
||||
|
||||
// Check for tag filter pattern: ":tag:XXX query" or ":tag:XXX"
|
||||
if let Some(rest) = trimmed.strip_prefix(":tag:") {
|
||||
// Find the end of the tag (space or end of string)
|
||||
if let Some(space_idx) = rest.find(' ') {
|
||||
let tag = rest[..space_idx].to_lowercase();
|
||||
let query_part = rest[space_idx + 1..].to_string();
|
||||
#[cfg(feature = "dev-logging")]
|
||||
debug!("[Filter] parse_query({:?}) -> tag={:?}, query={:?}", query, tag, query_part);
|
||||
return ParsedQuery {
|
||||
prefix: None,
|
||||
tag_filter: Some(tag),
|
||||
query: query_part,
|
||||
};
|
||||
} else {
|
||||
// Just the tag, no query yet
|
||||
let tag = rest.to_lowercase();
|
||||
return ParsedQuery {
|
||||
prefix: None,
|
||||
tag_filter: Some(tag),
|
||||
query: String::new(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Check for prefix patterns (with trailing space)
|
||||
let prefixes = [
|
||||
(":app ", ProviderType::Application),
|
||||
@@ -176,8 +217,11 @@ impl ProviderFilter {
|
||||
|
||||
for (prefix_str, provider) in prefixes {
|
||||
if let Some(rest) = trimmed.strip_prefix(prefix_str) {
|
||||
#[cfg(feature = "dev-logging")]
|
||||
debug!("[Filter] parse_query({:?}) -> prefix={:?}, query={:?}", query, provider, rest);
|
||||
return ParsedQuery {
|
||||
prefix: Some(provider),
|
||||
tag_filter: None,
|
||||
query: rest.to_string(),
|
||||
};
|
||||
}
|
||||
@@ -214,17 +258,26 @@ impl ProviderFilter {
|
||||
|
||||
for (prefix_str, provider) in partial_prefixes {
|
||||
if trimmed == prefix_str {
|
||||
#[cfg(feature = "dev-logging")]
|
||||
debug!("[Filter] parse_query({:?}) -> partial prefix {:?}", query, provider);
|
||||
return ParsedQuery {
|
||||
prefix: Some(provider),
|
||||
tag_filter: None,
|
||||
query: String::new(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
ParsedQuery {
|
||||
let result = ParsedQuery {
|
||||
prefix: None,
|
||||
tag_filter: None,
|
||||
query: query.to_string(),
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(feature = "dev-logging")]
|
||||
debug!("[Filter] parse_query({:?}) -> prefix={:?}, tag={:?}, query={:?}", query, result.prefix, result.tag_filter, result.query);
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Get enabled providers for UI display (sorted)
|
||||
|
||||
18
src/main.rs
18
src/main.rs
@@ -3,6 +3,7 @@ mod cli;
|
||||
mod config;
|
||||
mod data;
|
||||
mod filter;
|
||||
mod paths;
|
||||
mod providers;
|
||||
mod theme;
|
||||
mod ui;
|
||||
@@ -11,11 +12,26 @@ use app::OwlryApp;
|
||||
use cli::CliArgs;
|
||||
use log::{info, warn};
|
||||
|
||||
#[cfg(feature = "dev-logging")]
|
||||
use log::debug;
|
||||
|
||||
fn main() {
|
||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
|
||||
let default_level = if cfg!(feature = "dev-logging") { "debug" } else { "info" };
|
||||
|
||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(default_level))
|
||||
.format_timestamp_millis()
|
||||
.init();
|
||||
|
||||
let args = CliArgs::parse_args();
|
||||
|
||||
#[cfg(feature = "dev-logging")]
|
||||
{
|
||||
debug!("┌─────────────────────────────────────────┐");
|
||||
debug!("│ DEV-LOGGING: Verbose output enabled │");
|
||||
debug!("└─────────────────────────────────────────┘");
|
||||
debug!("CLI args: {:?}", args);
|
||||
}
|
||||
|
||||
info!("Starting Owlry launcher");
|
||||
|
||||
// Diagnostic: log critical environment variables
|
||||
|
||||
214
src/paths.rs
Normal file
214
src/paths.rs
Normal file
@@ -0,0 +1,214 @@
|
||||
//! Centralized path handling following XDG Base Directory Specification.
|
||||
//!
|
||||
//! XDG directories used:
|
||||
//! - `$XDG_CONFIG_HOME/owlry/` - User configuration (config.toml, themes/, style.css)
|
||||
//! - `$XDG_DATA_HOME/owlry/` - User data (scripts/, frecency.json)
|
||||
//! - `$XDG_CACHE_HOME/owlry/` - Cache files (future use)
|
||||
//!
|
||||
//! See: https://specifications.freedesktop.org/basedir-spec/latest/
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Application name used in XDG paths
|
||||
const APP_NAME: &str = "owlry";
|
||||
|
||||
// =============================================================================
|
||||
// XDG Base Directories
|
||||
// =============================================================================
|
||||
|
||||
/// Get XDG config home: `$XDG_CONFIG_HOME` or `~/.config`
|
||||
pub fn config_home() -> Option<PathBuf> {
|
||||
dirs::config_dir()
|
||||
}
|
||||
|
||||
/// Get XDG data home: `$XDG_DATA_HOME` or `~/.local/share`
|
||||
pub fn data_home() -> Option<PathBuf> {
|
||||
dirs::data_dir()
|
||||
}
|
||||
|
||||
/// Get XDG cache home: `$XDG_CACHE_HOME` or `~/.cache`
|
||||
#[allow(dead_code)]
|
||||
pub fn cache_home() -> Option<PathBuf> {
|
||||
dirs::cache_dir()
|
||||
}
|
||||
|
||||
/// Get user home directory
|
||||
pub fn home() -> Option<PathBuf> {
|
||||
dirs::home_dir()
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Owlry-specific directories
|
||||
// =============================================================================
|
||||
|
||||
/// Owlry config directory: `$XDG_CONFIG_HOME/owlry/`
|
||||
pub fn owlry_config_dir() -> Option<PathBuf> {
|
||||
config_home().map(|p| p.join(APP_NAME))
|
||||
}
|
||||
|
||||
/// Owlry data directory: `$XDG_DATA_HOME/owlry/`
|
||||
pub fn owlry_data_dir() -> Option<PathBuf> {
|
||||
data_home().map(|p| p.join(APP_NAME))
|
||||
}
|
||||
|
||||
/// Owlry cache directory: `$XDG_CACHE_HOME/owlry/`
|
||||
#[allow(dead_code)]
|
||||
pub fn owlry_cache_dir() -> Option<PathBuf> {
|
||||
cache_home().map(|p| p.join(APP_NAME))
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Config files
|
||||
// =============================================================================
|
||||
|
||||
/// Main config file: `$XDG_CONFIG_HOME/owlry/config.toml`
|
||||
pub fn config_file() -> Option<PathBuf> {
|
||||
owlry_config_dir().map(|p| p.join("config.toml"))
|
||||
}
|
||||
|
||||
/// Custom user stylesheet: `$XDG_CONFIG_HOME/owlry/style.css`
|
||||
pub fn custom_style_file() -> Option<PathBuf> {
|
||||
owlry_config_dir().map(|p| p.join("style.css"))
|
||||
}
|
||||
|
||||
/// User themes directory: `$XDG_CONFIG_HOME/owlry/themes/`
|
||||
pub fn themes_dir() -> Option<PathBuf> {
|
||||
owlry_config_dir().map(|p| p.join("themes"))
|
||||
}
|
||||
|
||||
/// Get path for a specific theme: `$XDG_CONFIG_HOME/owlry/themes/{name}.css`
|
||||
pub fn theme_file(name: &str) -> Option<PathBuf> {
|
||||
themes_dir().map(|p| p.join(format!("{}.css", name)))
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Data files
|
||||
// =============================================================================
|
||||
|
||||
/// User scripts directory: `$XDG_DATA_HOME/owlry/scripts/`
|
||||
pub fn scripts_dir() -> Option<PathBuf> {
|
||||
owlry_data_dir().map(|p| p.join("scripts"))
|
||||
}
|
||||
|
||||
/// Frecency data file: `$XDG_DATA_HOME/owlry/frecency.json`
|
||||
pub fn frecency_file() -> Option<PathBuf> {
|
||||
owlry_data_dir().map(|p| p.join("frecency.json"))
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// System directories
|
||||
// =============================================================================
|
||||
|
||||
/// System data directories for applications (XDG_DATA_DIRS)
|
||||
pub fn system_data_dirs() -> Vec<PathBuf> {
|
||||
let mut dirs = Vec::new();
|
||||
|
||||
// User data directory first
|
||||
if let Some(data) = data_home() {
|
||||
dirs.push(data.join("applications"));
|
||||
}
|
||||
|
||||
// System directories
|
||||
dirs.push(PathBuf::from("/usr/share/applications"));
|
||||
dirs.push(PathBuf::from("/usr/local/share/applications"));
|
||||
|
||||
// Flatpak directories
|
||||
if let Some(data) = data_home() {
|
||||
dirs.push(data.join("flatpak/exports/share/applications"));
|
||||
}
|
||||
dirs.push(PathBuf::from("/var/lib/flatpak/exports/share/applications"));
|
||||
|
||||
dirs
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// External application paths
|
||||
// =============================================================================
|
||||
|
||||
/// SSH config file: `~/.ssh/config`
|
||||
pub fn ssh_config() -> Option<PathBuf> {
|
||||
home().map(|p| p.join(".ssh").join("config"))
|
||||
}
|
||||
|
||||
/// Firefox profile directory: `~/.mozilla/firefox/`
|
||||
pub fn firefox_dir() -> Option<PathBuf> {
|
||||
home().map(|p| p.join(".mozilla").join("firefox"))
|
||||
}
|
||||
|
||||
/// Chromium-based browser bookmark paths (using XDG config where browsers support it)
|
||||
pub fn chromium_bookmark_paths() -> Vec<PathBuf> {
|
||||
let config = match config_home() {
|
||||
Some(c) => c,
|
||||
None => return Vec::new(),
|
||||
};
|
||||
|
||||
vec![
|
||||
// Google Chrome
|
||||
config.join("google-chrome/Default/Bookmarks"),
|
||||
// Chromium
|
||||
config.join("chromium/Default/Bookmarks"),
|
||||
// Brave
|
||||
config.join("BraveSoftware/Brave-Browser/Default/Bookmarks"),
|
||||
// Microsoft Edge
|
||||
config.join("microsoft-edge/Default/Bookmarks"),
|
||||
// Vivaldi
|
||||
config.join("vivaldi/Default/Bookmarks"),
|
||||
]
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Helper functions
|
||||
// =============================================================================
|
||||
|
||||
/// Ensure a directory exists, creating it if necessary
|
||||
pub fn ensure_dir(path: &PathBuf) -> std::io::Result<()> {
|
||||
if !path.exists() {
|
||||
std::fs::create_dir_all(path)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Ensure parent directory of a file exists
|
||||
pub fn ensure_parent_dir(path: &PathBuf) -> std::io::Result<()> {
|
||||
if let Some(parent) = path.parent() {
|
||||
if !parent.exists() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_paths_are_consistent() {
|
||||
// All owlry paths should be under XDG directories
|
||||
if let (Some(config), Some(data)) = (owlry_config_dir(), owlry_data_dir()) {
|
||||
assert!(config.ends_with("owlry"));
|
||||
assert!(data.ends_with("owlry"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_file_path() {
|
||||
if let Some(path) = config_file() {
|
||||
assert!(path.ends_with("config.toml"));
|
||||
assert!(path.to_string_lossy().contains("owlry"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_frecency_in_data_dir() {
|
||||
if let Some(path) = frecency_file() {
|
||||
assert!(path.ends_with("frecency.json"));
|
||||
// Should be in data dir, not config dir
|
||||
let path_str = path.to_string_lossy();
|
||||
assert!(
|
||||
path_str.contains(".local/share") || path_str.contains("XDG_DATA_HOME"),
|
||||
"frecency should be in data directory"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::{LaunchItem, Provider, ProviderType};
|
||||
use crate::paths;
|
||||
use freedesktop_desktop_entry::{DesktopEntry, Iter};
|
||||
use log::{debug, warn};
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Clean desktop file field codes from command string.
|
||||
/// Removes %f, %F, %u, %U, %d, %D, %n, %N, %i, %c, %k, %v, %m field codes
|
||||
@@ -75,25 +75,8 @@ impl ApplicationProvider {
|
||||
Self { items: Vec::new() }
|
||||
}
|
||||
|
||||
fn get_application_dirs() -> Vec<PathBuf> {
|
||||
let mut dirs = Vec::new();
|
||||
|
||||
// User applications
|
||||
if let Some(data_home) = dirs::data_dir() {
|
||||
dirs.push(data_home.join("applications"));
|
||||
}
|
||||
|
||||
// System applications
|
||||
dirs.push(PathBuf::from("/usr/share/applications"));
|
||||
dirs.push(PathBuf::from("/usr/local/share/applications"));
|
||||
|
||||
// Flatpak applications
|
||||
if let Some(data_home) = dirs::data_dir() {
|
||||
dirs.push(data_home.join("flatpak/exports/share/applications"));
|
||||
}
|
||||
dirs.push(PathBuf::from("/var/lib/flatpak/exports/share/applications"));
|
||||
|
||||
dirs
|
||||
fn get_application_dirs() -> Vec<std::path::PathBuf> {
|
||||
paths::system_data_dirs()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,6 +135,12 @@ impl Provider for ApplicationProvider {
|
||||
None => continue,
|
||||
};
|
||||
|
||||
// Extract categories as tags (lowercase for consistency)
|
||||
let tags: Vec<String> = desktop_entry
|
||||
.categories()
|
||||
.map(|cats| cats.into_iter().map(|s| s.to_lowercase()).collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
let item = LaunchItem {
|
||||
id: path.to_string_lossy().to_string(),
|
||||
name,
|
||||
@@ -160,6 +149,7 @@ impl Provider for ApplicationProvider {
|
||||
provider: ProviderType::Application,
|
||||
command: run_cmd,
|
||||
terminal: desktop_entry.terminal(),
|
||||
tags,
|
||||
};
|
||||
|
||||
self.items.push(item);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::paths;
|
||||
use crate::providers::{LaunchItem, Provider, ProviderType};
|
||||
use log::{debug, warn};
|
||||
use serde::Deserialize;
|
||||
@@ -27,8 +28,8 @@ impl BookmarksProvider {
|
||||
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"),
|
||||
let firefox_dir = match paths::firefox_dir() {
|
||||
Some(d) => d,
|
||||
None => return,
|
||||
};
|
||||
|
||||
@@ -99,29 +100,10 @@ impl BookmarksProvider {
|
||||
}
|
||||
|
||||
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 {
|
||||
// Chrome/Chromium bookmarks are in JSON format (XDG config paths)
|
||||
for path in paths::chromium_bookmark_paths() {
|
||||
if path.exists() {
|
||||
self.read_chrome_bookmarks(path);
|
||||
self.read_chrome_bookmarks(&path);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -175,6 +157,7 @@ impl BookmarksProvider {
|
||||
provider: ProviderType::Bookmarks,
|
||||
command: format!("xdg-open '{}'", url.replace('\'', "'\\''")),
|
||||
terminal: false,
|
||||
tags: Vec::new(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@ impl CalculatorProvider {
|
||||
provider: ProviderType::Calculator,
|
||||
command: format!("echo -n '{}' | wl-copy", result_str),
|
||||
terminal: false,
|
||||
tags: vec!["math".to_string()],
|
||||
})
|
||||
}
|
||||
Err(_) => None,
|
||||
@@ -111,6 +112,7 @@ impl CalculatorProvider {
|
||||
// Copy result to clipboard using wl-copy
|
||||
command: format!("sh -c 'echo -n \"{}\" | wl-copy'", result_str),
|
||||
terminal: false,
|
||||
tags: vec!["math".to_string()],
|
||||
};
|
||||
|
||||
debug!("Calculator result: {} = {}", expr, result_str);
|
||||
|
||||
@@ -99,6 +99,7 @@ impl ClipboardProvider {
|
||||
provider: ProviderType::Clipboard,
|
||||
command,
|
||||
terminal: false,
|
||||
tags: Vec::new(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -87,6 +87,7 @@ impl Provider for CommandProvider {
|
||||
provider: ProviderType::Command,
|
||||
command: name,
|
||||
terminal: false,
|
||||
tags: Vec::new(),
|
||||
};
|
||||
|
||||
self.items.push(item);
|
||||
|
||||
@@ -101,6 +101,7 @@ impl Provider for DmenuProvider {
|
||||
provider: ProviderType::Dmenu,
|
||||
command: line.to_string(),
|
||||
terminal: false,
|
||||
tags: Vec::new(),
|
||||
};
|
||||
|
||||
self.items.push(item);
|
||||
|
||||
@@ -406,6 +406,7 @@ impl EmojiProvider {
|
||||
// Copy emoji to clipboard using wl-copy
|
||||
command: format!("printf '%s' '{}' | wl-copy", emoji),
|
||||
terminal: false,
|
||||
tags: Vec::new(), // TODO: Extract category from emoji data
|
||||
});
|
||||
|
||||
// Store the search text for matching (not used directly but could be)
|
||||
@@ -441,6 +442,10 @@ mod tests {
|
||||
let mut provider = EmojiProvider::new();
|
||||
provider.refresh();
|
||||
assert!(provider.items().len() > 100);
|
||||
assert!(provider.items().iter().any(|i| i.name.contains("😀")));
|
||||
// Emoji character is in description, name is the human-readable name
|
||||
assert!(provider
|
||||
.items()
|
||||
.iter()
|
||||
.any(|i| i.description.as_ref().is_some_and(|d| d.contains("😀"))));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::paths;
|
||||
use crate::providers::{LaunchItem, ProviderType};
|
||||
use log::{debug, warn};
|
||||
use std::process::Command;
|
||||
@@ -106,7 +107,7 @@ impl FileSearchProvider {
|
||||
|
||||
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 home = paths::home().unwrap_or_default();
|
||||
|
||||
let output = match Command::new("fd")
|
||||
.args([
|
||||
@@ -132,7 +133,7 @@ impl FileSearchProvider {
|
||||
}
|
||||
|
||||
fn search_with_locate(&self, pattern: &str) -> Vec<LaunchItem> {
|
||||
let home = dirs::home_dir().unwrap_or_default();
|
||||
let home = paths::home().unwrap_or_default();
|
||||
|
||||
let output = match Command::new("locate")
|
||||
.args([
|
||||
@@ -190,6 +191,7 @@ impl FileSearchProvider {
|
||||
provider: ProviderType::Files,
|
||||
command,
|
||||
terminal: false,
|
||||
tags: Vec::new(),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
|
||||
@@ -30,6 +30,9 @@ use fuzzy_matcher::FuzzyMatcher;
|
||||
use fuzzy_matcher::skim::SkimMatcherV2;
|
||||
use log::info;
|
||||
|
||||
#[cfg(feature = "dev-logging")]
|
||||
use log::debug;
|
||||
|
||||
use crate::data::FrecencyStore;
|
||||
|
||||
/// Represents a single searchable/launchable item
|
||||
@@ -43,6 +46,8 @@ pub struct LaunchItem {
|
||||
pub provider: ProviderType,
|
||||
pub command: String,
|
||||
pub terminal: bool,
|
||||
/// Tags/categories for filtering (e.g., from .desktop Categories)
|
||||
pub tags: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
@@ -279,7 +284,7 @@ impl ProviderManager {
|
||||
results
|
||||
}
|
||||
|
||||
/// Search with frecency boosting and calculator support
|
||||
/// Search with frecency boosting, calculator support, and tag filtering
|
||||
pub fn search_with_frecency(
|
||||
&mut self,
|
||||
query: &str,
|
||||
@@ -287,13 +292,18 @@ impl ProviderManager {
|
||||
filter: &crate::filter::ProviderFilter,
|
||||
frecency: &FrecencyStore,
|
||||
frecency_weight: f64,
|
||||
tag_filter: Option<&str>,
|
||||
) -> Vec<(LaunchItem, i64)> {
|
||||
#[cfg(feature = "dev-logging")]
|
||||
debug!("[Search] query={:?}, max={}, frecency_weight={}", query, max_results, frecency_weight);
|
||||
|
||||
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
|
||||
#[cfg(feature = "dev-logging")]
|
||||
debug!("[Search] Calculator result: {}", calc_result.name);
|
||||
results.push((calc_result, 10000));
|
||||
}
|
||||
}
|
||||
@@ -323,6 +333,8 @@ impl ProviderManager {
|
||||
// Check for file search query
|
||||
if FileSearchProvider::is_file_query(query) {
|
||||
let file_results = self.filesearch.evaluate(query);
|
||||
#[cfg(feature = "dev-logging")]
|
||||
debug!("[Search] File search returned {} results", file_results.len());
|
||||
for (idx, item) in file_results.into_iter().enumerate() {
|
||||
// Score decreases for each result to maintain order
|
||||
results.push((item, 8000 - idx as i64));
|
||||
@@ -343,6 +355,14 @@ impl ProviderManager {
|
||||
.iter()
|
||||
.filter(|p| filter.is_active(p.provider_type()))
|
||||
.flat_map(|p| p.items().iter().cloned())
|
||||
.filter(|item| {
|
||||
// Apply tag filter if present
|
||||
if let Some(tag) = tag_filter {
|
||||
item.tags.iter().any(|t| t.to_lowercase().contains(tag))
|
||||
} else {
|
||||
true
|
||||
}
|
||||
})
|
||||
.map(|item| {
|
||||
let frecency_score = frecency.get_score(&item.id);
|
||||
let boosted = (frecency_score * frecency_weight * 100.0) as i64;
|
||||
@@ -355,24 +375,43 @@ impl ProviderManager {
|
||||
return items;
|
||||
}
|
||||
|
||||
// Regular search with frecency boost
|
||||
// Regular search with frecency boost and tag matching
|
||||
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| {
|
||||
// Apply tag filter if present
|
||||
if let Some(tag) = tag_filter {
|
||||
if !item.tags.iter().any(|t| t.to_lowercase().contains(tag)) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
// Also match against tags (lower weight)
|
||||
let tag_score = item
|
||||
.tags
|
||||
.iter()
|
||||
.filter_map(|t| self.matcher.fuzzy_match(t, query))
|
||||
.max()
|
||||
.map(|s| s / 3); // Lower weight for tag matches
|
||||
|
||||
let base_score = match (name_score, desc_score, tag_score) {
|
||||
(Some(n), Some(d), Some(t)) => Some(n.max(d).max(t)),
|
||||
(Some(n), Some(d), None) => Some(n.max(d)),
|
||||
(Some(n), None, Some(t)) => Some(n.max(t)),
|
||||
(Some(n), None, None) => Some(n),
|
||||
(None, Some(d), Some(t)) => Some((d / 2).max(t)),
|
||||
(None, Some(d), None) => Some(d / 2),
|
||||
(None, None, Some(t)) => Some(t),
|
||||
(None, None, None) => None,
|
||||
};
|
||||
|
||||
base_score.map(|s| {
|
||||
@@ -387,6 +426,18 @@ impl ProviderManager {
|
||||
results.extend(search_results);
|
||||
results.sort_by(|a, b| b.1.cmp(&a.1));
|
||||
results.truncate(max_results);
|
||||
|
||||
#[cfg(feature = "dev-logging")]
|
||||
{
|
||||
debug!("[Search] Returning {} results", results.len());
|
||||
for (i, (item, score)) in results.iter().take(5).enumerate() {
|
||||
debug!("[Search] #{}: {} (score={}, provider={:?})", i + 1, item.name, score, item.provider);
|
||||
}
|
||||
if results.len() > 5 {
|
||||
debug!("[Search] ... and {} more", results.len() - 5);
|
||||
}
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use crate::paths;
|
||||
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/
|
||||
/// Custom scripts provider - runs user scripts from `$XDG_DATA_HOME/owlry/scripts/`
|
||||
pub struct ScriptsProvider {
|
||||
items: Vec<LaunchItem>,
|
||||
}
|
||||
@@ -14,14 +15,10 @@ impl ScriptsProvider {
|
||||
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() {
|
||||
let scripts_dir = match paths::scripts_dir() {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
debug!("Could not determine scripts directory");
|
||||
@@ -32,7 +29,7 @@ impl ScriptsProvider {
|
||||
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) {
|
||||
if let Err(e) = paths::ensure_dir(&scripts_dir) {
|
||||
warn!("Failed to create scripts directory: {}", e);
|
||||
}
|
||||
return;
|
||||
@@ -90,6 +87,7 @@ impl ScriptsProvider {
|
||||
provider: ProviderType::Scripts,
|
||||
command: path.to_string_lossy().to_string(),
|
||||
terminal: false,
|
||||
tags: vec!["script".to_string()],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::paths;
|
||||
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 {
|
||||
@@ -27,8 +27,8 @@ impl SshProvider {
|
||||
self.terminal_command = terminal.to_string();
|
||||
}
|
||||
|
||||
fn ssh_config_path() -> Option<PathBuf> {
|
||||
dirs::home_dir().map(|p| p.join(".ssh").join("config"))
|
||||
fn ssh_config_path() -> Option<std::path::PathBuf> {
|
||||
paths::ssh_config()
|
||||
}
|
||||
|
||||
fn parse_ssh_config(&mut self) {
|
||||
@@ -161,6 +161,7 @@ impl SshProvider {
|
||||
provider: ProviderType::Ssh,
|
||||
command,
|
||||
terminal: false, // We're already wrapping in terminal
|
||||
tags: vec!["ssh".to_string()],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ impl SystemProvider {
|
||||
provider: ProviderType::System,
|
||||
command: command.to_string(),
|
||||
terminal: false,
|
||||
tags: vec!["power".to_string(), "system".to_string()],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ impl UuctlProvider {
|
||||
provider: ProviderType::Uuctl,
|
||||
command: format!("systemctl --user restart {}", unit_name),
|
||||
terminal: false,
|
||||
tags: vec!["systemd".to_string(), "service".to_string()],
|
||||
});
|
||||
|
||||
actions.push(LaunchItem {
|
||||
@@ -56,6 +57,7 @@ impl UuctlProvider {
|
||||
provider: ProviderType::Uuctl,
|
||||
command: format!("systemctl --user stop {}", unit_name),
|
||||
terminal: false,
|
||||
tags: vec!["systemd".to_string(), "service".to_string()],
|
||||
});
|
||||
|
||||
actions.push(LaunchItem {
|
||||
@@ -66,6 +68,7 @@ impl UuctlProvider {
|
||||
provider: ProviderType::Uuctl,
|
||||
command: format!("systemctl --user reload {}", unit_name),
|
||||
terminal: false,
|
||||
tags: vec!["systemd".to_string(), "service".to_string()],
|
||||
});
|
||||
|
||||
actions.push(LaunchItem {
|
||||
@@ -76,6 +79,7 @@ impl UuctlProvider {
|
||||
provider: ProviderType::Uuctl,
|
||||
command: format!("systemctl --user kill {}", unit_name),
|
||||
terminal: false,
|
||||
tags: vec!["systemd".to_string(), "service".to_string()],
|
||||
});
|
||||
} else {
|
||||
actions.push(LaunchItem {
|
||||
@@ -86,6 +90,7 @@ impl UuctlProvider {
|
||||
provider: ProviderType::Uuctl,
|
||||
command: format!("systemctl --user start {}", unit_name),
|
||||
terminal: false,
|
||||
tags: vec!["systemd".to_string(), "service".to_string()],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -98,6 +103,7 @@ impl UuctlProvider {
|
||||
provider: ProviderType::Uuctl,
|
||||
command: format!("systemctl --user status {}", unit_name),
|
||||
terminal: true,
|
||||
tags: vec!["systemd".to_string(), "service".to_string()],
|
||||
});
|
||||
|
||||
actions.push(LaunchItem {
|
||||
@@ -108,6 +114,7 @@ impl UuctlProvider {
|
||||
provider: ProviderType::Uuctl,
|
||||
command: format!("journalctl --user -u {} -f", unit_name),
|
||||
terminal: true,
|
||||
tags: vec!["systemd".to_string(), "service".to_string()],
|
||||
});
|
||||
|
||||
actions.push(LaunchItem {
|
||||
@@ -118,6 +125,7 @@ impl UuctlProvider {
|
||||
provider: ProviderType::Uuctl,
|
||||
command: format!("systemctl --user enable {}", unit_name),
|
||||
terminal: false,
|
||||
tags: vec!["systemd".to_string(), "service".to_string()],
|
||||
});
|
||||
|
||||
actions.push(LaunchItem {
|
||||
@@ -128,6 +136,7 @@ impl UuctlProvider {
|
||||
provider: ProviderType::Uuctl,
|
||||
command: format!("systemctl --user disable {}", unit_name),
|
||||
terminal: false,
|
||||
tags: vec!["systemd".to_string(), "service".to_string()],
|
||||
});
|
||||
|
||||
actions
|
||||
@@ -189,6 +198,7 @@ impl UuctlProvider {
|
||||
provider: ProviderType::Uuctl,
|
||||
command: submenu_data, // Special marker for submenu
|
||||
terminal: false,
|
||||
tags: vec!["systemd".to_string(), "service".to_string()],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -136,6 +136,7 @@ impl WebSearchProvider {
|
||||
provider: ProviderType::WebSearch,
|
||||
command,
|
||||
terminal: false,
|
||||
tags: vec!["web".to_string(), "search".to_string()],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,6 +315,22 @@ scrollbar slider:active {
|
||||
background-color: var(--owlry-accent, @theme_selected_bg_color);
|
||||
}
|
||||
|
||||
/* Tag badges */
|
||||
.owlry-tag-badge {
|
||||
font-size: calc(var(--owlry-font-size, 14px) - 4px);
|
||||
font-weight: 500;
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
background-color: alpha(var(--owlry-border, @borders), 0.3);
|
||||
color: var(--owlry-text-secondary, alpha(@theme_fg_color, 0.6));
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.owlry-result-row:selected .owlry-tag-badge {
|
||||
background-color: alpha(var(--owlry-accent-bright, @theme_selected_fg_color), 0.2);
|
||||
color: var(--owlry-accent-bright, @theme_selected_fg_color);
|
||||
}
|
||||
|
||||
/* Text selection */
|
||||
selection {
|
||||
background-color: alpha(var(--owlry-accent, @theme_selected_bg_color), 0.3);
|
||||
@@ -10,6 +10,10 @@ use gtk4::{
|
||||
ListBoxRow, Orientation, ScrolledWindow, SelectionMode, ToggleButton,
|
||||
};
|
||||
use log::info;
|
||||
|
||||
#[cfg(feature = "dev-logging")]
|
||||
use log::debug;
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::process::Command;
|
||||
@@ -44,6 +48,8 @@ pub struct MainWindow {
|
||||
hints_label: Label,
|
||||
filter_buttons: Rc<RefCell<HashMap<ProviderType, ToggleButton>>>,
|
||||
submenu_state: Rc<RefCell<SubmenuState>>,
|
||||
/// Parsed tab config (ProviderTypes for cycling)
|
||||
tab_order: Rc<Vec<ProviderType>>,
|
||||
}
|
||||
|
||||
impl MainWindow {
|
||||
@@ -104,8 +110,17 @@ impl MainWindow {
|
||||
.build();
|
||||
filter_tabs.add_css_class("owlry-filter-tabs");
|
||||
|
||||
// Create toggle buttons for each provider
|
||||
let filter_buttons = Self::create_filter_buttons(&filter_tabs, &filter);
|
||||
// Parse tabs config to ProviderTypes
|
||||
let tab_order: Vec<ProviderType> = cfg
|
||||
.general
|
||||
.tabs
|
||||
.iter()
|
||||
.filter_map(|s| s.parse().ok())
|
||||
.collect();
|
||||
let tab_order = Rc::new(tab_order);
|
||||
|
||||
// Create toggle buttons for each provider (from config)
|
||||
let filter_buttons = Self::create_filter_buttons(&filter_tabs, &filter, &cfg.general.tabs);
|
||||
let filter_buttons = Rc::new(RefCell::new(filter_buttons));
|
||||
|
||||
header_box.append(&mode_label);
|
||||
@@ -143,7 +158,7 @@ impl MainWindow {
|
||||
hints_box.add_css_class("owlry-hints");
|
||||
|
||||
let hints_label = Label::builder()
|
||||
.label("Tab: cycle mode ↑↓: navigate Enter: launch Esc: close = calc ? web :app :cmd")
|
||||
.label(&Self::build_hints(&cfg.providers))
|
||||
.halign(gtk4::Align::Center)
|
||||
.hexpand(true)
|
||||
.build();
|
||||
@@ -173,6 +188,7 @@ impl MainWindow {
|
||||
hints_label,
|
||||
filter_buttons,
|
||||
submenu_state: Rc::new(RefCell::new(SubmenuState::default())),
|
||||
tab_order,
|
||||
};
|
||||
|
||||
main_window.setup_signals();
|
||||
@@ -187,38 +203,31 @@ impl MainWindow {
|
||||
fn create_filter_buttons(
|
||||
container: &GtkBox,
|
||||
filter: &Rc<RefCell<ProviderFilter>>,
|
||||
tabs: &[String],
|
||||
) -> HashMap<ProviderType, ToggleButton> {
|
||||
let providers = [
|
||||
(ProviderType::Application, "Apps", "Ctrl+1"),
|
||||
(ProviderType::Command, "Cmds", "Ctrl+2"),
|
||||
(ProviderType::Uuctl, "uuctl", "Ctrl+3"),
|
||||
];
|
||||
|
||||
let mut buttons = HashMap::new();
|
||||
|
||||
for (provider_type, label, shortcut) in providers {
|
||||
// Parse tab strings to ProviderType and create buttons
|
||||
for (idx, tab_str) in tabs.iter().enumerate() {
|
||||
let provider_type: ProviderType = match tab_str.parse() {
|
||||
Ok(pt) => pt,
|
||||
Err(e) => {
|
||||
log::warn!("Invalid tab config '{}': {}", tab_str, e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let label = Self::provider_tab_label(provider_type);
|
||||
let shortcut = format!("Ctrl+{}", idx + 1);
|
||||
|
||||
let button = ToggleButton::builder()
|
||||
.label(label)
|
||||
.tooltip_text(shortcut)
|
||||
.tooltip_text(&shortcut)
|
||||
.active(filter.borrow().is_enabled(provider_type))
|
||||
.build();
|
||||
|
||||
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::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",
|
||||
};
|
||||
let css_class = Self::provider_css_class(provider_type);
|
||||
button.add_css_class(css_class);
|
||||
|
||||
container.append(&button);
|
||||
@@ -228,6 +237,44 @@ impl MainWindow {
|
||||
buttons
|
||||
}
|
||||
|
||||
/// Get display label for a provider tab
|
||||
fn provider_tab_label(provider: ProviderType) -> &'static str {
|
||||
match provider {
|
||||
ProviderType::Application => "Apps",
|
||||
ProviderType::Bookmarks => "Bookmarks",
|
||||
ProviderType::Calculator => "Calc",
|
||||
ProviderType::Clipboard => "Clip",
|
||||
ProviderType::Command => "Cmds",
|
||||
ProviderType::Dmenu => "Dmenu",
|
||||
ProviderType::Emoji => "Emoji",
|
||||
ProviderType::Files => "Files",
|
||||
ProviderType::Scripts => "Scripts",
|
||||
ProviderType::Ssh => "SSH",
|
||||
ProviderType::System => "System",
|
||||
ProviderType::Uuctl => "uuctl",
|
||||
ProviderType::WebSearch => "Web",
|
||||
}
|
||||
}
|
||||
|
||||
/// Get CSS class for a provider
|
||||
fn provider_css_class(provider: ProviderType) -> &'static str {
|
||||
match provider {
|
||||
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::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",
|
||||
}
|
||||
}
|
||||
|
||||
fn build_placeholder(filter: &ProviderFilter) -> String {
|
||||
let active: Vec<&str> = filter
|
||||
.enabled_providers()
|
||||
@@ -252,6 +299,52 @@ impl MainWindow {
|
||||
format!("Search {}...", active.join(", "))
|
||||
}
|
||||
|
||||
/// Build dynamic hints based on enabled providers
|
||||
fn build_hints(config: &crate::config::ProvidersConfig) -> String {
|
||||
let mut parts: Vec<String> = vec![
|
||||
"Tab: cycle".to_string(),
|
||||
"↑↓: nav".to_string(),
|
||||
"Enter: launch".to_string(),
|
||||
"Esc: close".to_string(),
|
||||
];
|
||||
|
||||
// Add trigger hints for enabled dynamic providers
|
||||
if config.calculator {
|
||||
parts.push("= calc".to_string());
|
||||
}
|
||||
if config.websearch {
|
||||
parts.push("? web".to_string());
|
||||
}
|
||||
if config.files {
|
||||
parts.push("/ files".to_string());
|
||||
}
|
||||
|
||||
// Add prefix hints for static providers
|
||||
let mut prefixes = Vec::new();
|
||||
if config.system {
|
||||
prefixes.push(":sys");
|
||||
}
|
||||
if config.emoji {
|
||||
prefixes.push(":emoji");
|
||||
}
|
||||
if config.ssh {
|
||||
prefixes.push(":ssh");
|
||||
}
|
||||
if config.clipboard {
|
||||
prefixes.push(":clip");
|
||||
}
|
||||
if config.bookmarks {
|
||||
prefixes.push(":bm");
|
||||
}
|
||||
|
||||
// Only show first few prefixes to avoid overflow
|
||||
if !prefixes.is_empty() {
|
||||
parts.push(prefixes[..prefixes.len().min(4)].join(" "));
|
||||
}
|
||||
|
||||
parts.join(" ")
|
||||
}
|
||||
|
||||
/// Scroll the given row into view within the scrolled window
|
||||
fn scroll_to_row(scrolled: &ScrolledWindow, results_list: &ListBox, row: &ListBoxRow) {
|
||||
let vadj = scrolled.vadjustment();
|
||||
@@ -298,6 +391,9 @@ impl MainWindow {
|
||||
display_name: &str,
|
||||
is_active: bool,
|
||||
) {
|
||||
#[cfg(feature = "dev-logging")]
|
||||
debug!("[UI] Entering submenu for service: {} (active={})", unit_name, is_active);
|
||||
|
||||
let actions = UuctlProvider::actions_for_service(unit_name, display_name, is_active);
|
||||
|
||||
// Save current state
|
||||
@@ -340,7 +436,11 @@ impl MainWindow {
|
||||
hints_label: &Label,
|
||||
search_entry: &Entry,
|
||||
filter: &Rc<RefCell<ProviderFilter>>,
|
||||
config: &Rc<RefCell<Config>>,
|
||||
) {
|
||||
#[cfg(feature = "dev-logging")]
|
||||
debug!("[UI] Exiting submenu");
|
||||
|
||||
let saved_search = {
|
||||
let mut state = submenu_state.borrow_mut();
|
||||
state.active = false;
|
||||
@@ -350,7 +450,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 = calc ? web :app :cmd");
|
||||
hints_label.set_label(&Self::build_hints(&config.borrow().providers));
|
||||
search_entry.set_placeholder_text(Some(&Self::build_placeholder(&filter.borrow())));
|
||||
search_entry.set_text(&saved_search);
|
||||
|
||||
@@ -450,7 +550,7 @@ impl MainWindow {
|
||||
let results: Vec<LaunchItem> = if use_frecency {
|
||||
providers
|
||||
.borrow_mut()
|
||||
.search_with_frecency(&parsed.query, max_results, &filter.borrow(), &frecency.borrow(), frecency_weight)
|
||||
.search_with_frecency(&parsed.query, max_results, &filter.borrow(), &frecency.borrow(), frecency_weight, parsed.tag_filter.as_deref())
|
||||
.into_iter()
|
||||
.map(|(item, _)| item)
|
||||
.collect()
|
||||
@@ -553,17 +653,21 @@ impl MainWindow {
|
||||
let scrolled = self.scrolled.clone();
|
||||
let search_entry = self.search_entry.clone();
|
||||
let _current_results = self.current_results.clone();
|
||||
let _config = self.config.clone();
|
||||
let config = self.config.clone();
|
||||
let filter = self.filter.clone();
|
||||
let filter_buttons = self.filter_buttons.clone();
|
||||
let mode_label = self.mode_label.clone();
|
||||
let hints_label = self.hints_label.clone();
|
||||
let submenu_state = self.submenu_state.clone();
|
||||
let tab_order = self.tab_order.clone();
|
||||
|
||||
key_controller.connect_key_pressed(move |_, key, _, modifiers| {
|
||||
let ctrl = modifiers.contains(gtk4::gdk::ModifierType::CONTROL_MASK);
|
||||
let shift = modifiers.contains(gtk4::gdk::ModifierType::SHIFT_MASK);
|
||||
|
||||
#[cfg(feature = "dev-logging")]
|
||||
debug!("[UI] Key pressed: {:?} (ctrl={}, shift={})", key, ctrl, shift);
|
||||
|
||||
match key {
|
||||
Key::Escape => {
|
||||
// If in submenu, exit submenu first
|
||||
@@ -574,6 +678,7 @@ impl MainWindow {
|
||||
&hints_label,
|
||||
&search_entry,
|
||||
&filter,
|
||||
&config,
|
||||
);
|
||||
gtk4::glib::Propagation::Stop
|
||||
} else {
|
||||
@@ -590,6 +695,7 @@ impl MainWindow {
|
||||
&hints_label,
|
||||
&search_entry,
|
||||
&filter,
|
||||
&config,
|
||||
);
|
||||
gtk4::glib::Propagation::Stop
|
||||
} else {
|
||||
@@ -631,6 +737,7 @@ impl MainWindow {
|
||||
&filter_buttons,
|
||||
&search_entry,
|
||||
&mode_label,
|
||||
&tab_order,
|
||||
!shift,
|
||||
);
|
||||
}
|
||||
@@ -643,45 +750,37 @@ impl MainWindow {
|
||||
&filter_buttons,
|
||||
&search_entry,
|
||||
&mode_label,
|
||||
&tab_order,
|
||||
false,
|
||||
);
|
||||
}
|
||||
gtk4::glib::Propagation::Stop
|
||||
}
|
||||
// Ctrl+1/2/3 toggle specific providers (only when not in submenu)
|
||||
Key::_1 if ctrl => {
|
||||
// Ctrl+1-9 toggle specific providers based on tab order (only when not in submenu)
|
||||
Key::_1 | Key::_2 | Key::_3 | Key::_4 | Key::_5 |
|
||||
Key::_6 | Key::_7 | Key::_8 | Key::_9 if ctrl => {
|
||||
if !submenu_state.borrow().active {
|
||||
Self::toggle_provider_button(
|
||||
ProviderType::Application,
|
||||
&filter,
|
||||
&filter_buttons,
|
||||
&search_entry,
|
||||
&mode_label,
|
||||
);
|
||||
}
|
||||
gtk4::glib::Propagation::Stop
|
||||
}
|
||||
Key::_2 if ctrl => {
|
||||
if !submenu_state.borrow().active {
|
||||
Self::toggle_provider_button(
|
||||
ProviderType::Command,
|
||||
&filter,
|
||||
&filter_buttons,
|
||||
&search_entry,
|
||||
&mode_label,
|
||||
);
|
||||
}
|
||||
gtk4::glib::Propagation::Stop
|
||||
}
|
||||
Key::_3 if ctrl => {
|
||||
if !submenu_state.borrow().active {
|
||||
Self::toggle_provider_button(
|
||||
ProviderType::Uuctl,
|
||||
&filter,
|
||||
&filter_buttons,
|
||||
&search_entry,
|
||||
&mode_label,
|
||||
);
|
||||
let idx = match key {
|
||||
Key::_1 => 0,
|
||||
Key::_2 => 1,
|
||||
Key::_3 => 2,
|
||||
Key::_4 => 3,
|
||||
Key::_5 => 4,
|
||||
Key::_6 => 5,
|
||||
Key::_7 => 6,
|
||||
Key::_8 => 7,
|
||||
Key::_9 => 8,
|
||||
_ => return gtk4::glib::Propagation::Proceed,
|
||||
};
|
||||
if let Some(&provider) = tab_order.get(idx) {
|
||||
Self::toggle_provider_button(
|
||||
provider,
|
||||
&filter,
|
||||
&filter_buttons,
|
||||
&search_entry,
|
||||
&mode_label,
|
||||
);
|
||||
}
|
||||
}
|
||||
gtk4::glib::Propagation::Stop
|
||||
}
|
||||
@@ -735,24 +834,24 @@ impl MainWindow {
|
||||
buttons: &Rc<RefCell<HashMap<ProviderType, ToggleButton>>>,
|
||||
entry: &Entry,
|
||||
mode_label: &Label,
|
||||
tab_order: &[ProviderType],
|
||||
forward: bool,
|
||||
) {
|
||||
let order = [
|
||||
ProviderType::Application,
|
||||
ProviderType::Command,
|
||||
ProviderType::Uuctl,
|
||||
];
|
||||
if tab_order.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let current = filter.borrow().enabled_providers();
|
||||
|
||||
let next = if current.len() == 1 {
|
||||
let idx = order.iter().position(|p| p == ¤t[0]).unwrap_or(0);
|
||||
let idx = tab_order.iter().position(|p| p == ¤t[0]).unwrap_or(0);
|
||||
if forward {
|
||||
order[(idx + 1) % order.len()]
|
||||
tab_order[(idx + 1) % tab_order.len()]
|
||||
} else {
|
||||
order[(idx + order.len() - 1) % order.len()]
|
||||
tab_order[(idx + tab_order.len() - 1) % tab_order.len()]
|
||||
}
|
||||
} else {
|
||||
ProviderType::Application
|
||||
tab_order[0]
|
||||
};
|
||||
|
||||
{
|
||||
@@ -800,7 +899,7 @@ impl MainWindow {
|
||||
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)
|
||||
.search_with_frecency(query, max_results, &self.filter.borrow(), &self.frecency.borrow(), frecency_weight, None)
|
||||
.into_iter()
|
||||
.map(|(item, _)| item)
|
||||
.collect()
|
||||
@@ -833,10 +932,15 @@ impl MainWindow {
|
||||
// Record this launch for frecency tracking
|
||||
if config.providers.frecency {
|
||||
frecency.borrow_mut().record_launch(&item.id);
|
||||
#[cfg(feature = "dev-logging")]
|
||||
debug!("[UI] Recorded frecency launch for: {}", item.id);
|
||||
}
|
||||
|
||||
info!("Launching: {} ({})", item.name, item.command);
|
||||
|
||||
#[cfg(feature = "dev-logging")]
|
||||
debug!("[UI] Launch details: terminal={}, provider={:?}", item.terminal, item.provider);
|
||||
|
||||
let cmd = if item.terminal {
|
||||
format!("{} -e {}", config.general.terminal_command, item.command)
|
||||
} else {
|
||||
|
||||
@@ -82,6 +82,25 @@ impl ResultRow {
|
||||
text_box.append(&name_label);
|
||||
}
|
||||
|
||||
// Tag badges (show first 3 tags)
|
||||
if !item.tags.is_empty() {
|
||||
let tags_box = GtkBox::builder()
|
||||
.orientation(Orientation::Horizontal)
|
||||
.spacing(4)
|
||||
.halign(gtk4::Align::Start)
|
||||
.build();
|
||||
|
||||
for tag in item.tags.iter().take(3) {
|
||||
let tag_label = Label::builder()
|
||||
.label(tag)
|
||||
.build();
|
||||
tag_label.add_css_class("owlry-tag-badge");
|
||||
tags_box.append(&tag_label);
|
||||
}
|
||||
|
||||
text_box.append(&tags_box);
|
||||
}
|
||||
|
||||
// Provider badge
|
||||
let badge = Label::builder()
|
||||
.label(&item.provider.to_string())
|
||||
|
||||
Reference in New Issue
Block a user