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
|
/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]]
|
[[package]]
|
||||||
name = "owlry"
|
name = "owlry"
|
||||||
version = "0.3.2"
|
version = "0.3.7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
|
|||||||
13
Cargo.toml
13
Cargo.toml
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "owlry"
|
name = "owlry"
|
||||||
version = "0.3.2"
|
version = "0.3.7"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.90"
|
rust-version = "1.90"
|
||||||
description = "A lightweight, owl-themed application launcher for Wayland"
|
description = "A lightweight, owl-themed application launcher for Wayland"
|
||||||
@@ -55,6 +55,11 @@ serde_json = "1"
|
|||||||
# Date/time for frecency calculations
|
# Date/time for frecency calculations
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
# Enable verbose debug logging (for development/testing builds)
|
||||||
|
dev-logging = []
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = true
|
lto = true
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
@@ -65,3 +70,9 @@ opt-level = "z" # Optimize for size
|
|||||||
[profile.dev]
|
[profile.dev]
|
||||||
opt-level = 0
|
opt-level = 0
|
||||||
debug = true
|
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
|
## Features
|
||||||
|
|
||||||
- **Provider-based architecture** - Search applications, commands, system actions, SSH hosts, clipboard history, bookmarks, emoji, and more
|
- **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
|
- **Fuzzy search with tags** — Fast matching across names, descriptions, and category tags
|
||||||
- **Filter tabs & prefixes** - Scope searches with UI tabs or `:app`, `:cmd`, `:sys` prefixes
|
- **Configurable tabs** — Customize header tabs and keyboard shortcuts
|
||||||
- **Calculator** - Quick math with `= 5+3` or `calc sin(pi/2)`
|
- **Filter prefixes** — Scope searches with `:app`, `:cmd`, `:tag:development`, etc.
|
||||||
- **Web search** - Search the web with `? query` or `web query`
|
- **Calculator** — Quick math with `= 5+3` or `calc sin(pi/2)`
|
||||||
- **File search** - Find files with `/ filename` or `find config` (requires `fd` or `locate`)
|
- **Web search** — Search the web with `? query`
|
||||||
- **Frecency ranking** - Frequently/recently used items rank higher
|
- **File search** — Find files with `/ filename` (requires `fd` or `locate`)
|
||||||
- **GTK4 theming** - Respects system theme by default, with optional custom themes
|
- **Frecency ranking** — Frequently/recently used items rank higher
|
||||||
- **Wayland native** - Uses Layer Shell for proper overlay behavior
|
- **GTK4 theming** — System theme by default, with 9 built-in themes
|
||||||
|
- **Wayland native** — Uses Layer Shell for proper overlay behavior
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### Arch Linux (AUR)
|
### Arch Linux (AUR)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Using yay
|
|
||||||
yay -S owlry
|
yay -S owlry
|
||||||
|
# or
|
||||||
# Using paru
|
|
||||||
paru -S owlry
|
paru -S owlry
|
||||||
```
|
```
|
||||||
|
|
||||||
### Build from source
|
### Build from Source
|
||||||
|
|
||||||
#### Dependencies
|
|
||||||
|
|
||||||
|
**Dependencies:**
|
||||||
```bash
|
```bash
|
||||||
# Arch Linux
|
# Arch Linux
|
||||||
sudo pacman -S gtk4 gtk4-layer-shell
|
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
|
sudo dnf install gtk4-devel gtk4-layer-shell-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Optional dependencies
|
**Optional dependencies:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# For clipboard history
|
# Clipboard history
|
||||||
sudo pacman -S cliphist wl-clipboard
|
sudo pacman -S cliphist wl-clipboard
|
||||||
|
|
||||||
# For file search
|
# File search (choose one)
|
||||||
sudo pacman -S fd # or: mlocate
|
sudo pacman -S fd # recommended
|
||||||
|
sudo pacman -S mlocate # alternative
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Build
|
**Build (requires Rust 1.90+):**
|
||||||
|
|
||||||
Requires Rust 1.90 or later.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://somegit.dev/Owlibou/owlry.git
|
git clone https://somegit.dev/Owlibou/owlry.git
|
||||||
cd owlry
|
cd owlry
|
||||||
cargo build --release
|
cargo build --release
|
||||||
|
# Binary: target/release/owlry
|
||||||
```
|
```
|
||||||
|
|
||||||
The binary will be at `target/release/owlry`.
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Launch with default settings (GTK theme, all providers)
|
owlry # Launch with defaults
|
||||||
owlry
|
owlry --mode app # Applications only
|
||||||
|
owlry --providers app,cmd # Specific providers
|
||||||
# Launch with only applications
|
owlry --help # Show all options
|
||||||
owlry --mode app
|
|
||||||
|
|
||||||
# Launch with specific providers
|
|
||||||
owlry --providers app,cmd
|
|
||||||
|
|
||||||
# Show help
|
|
||||||
owlry --help
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Keyboard Shortcuts
|
### Keyboard Shortcuts
|
||||||
@@ -91,135 +78,84 @@ owlry --help
|
|||||||
|-----|--------|
|
|-----|--------|
|
||||||
| `Enter` | Launch selected item |
|
| `Enter` | Launch selected item |
|
||||||
| `Escape` | Close launcher / exit submenu |
|
| `Escape` | Close launcher / exit submenu |
|
||||||
| `Up` / `Down` | Navigate results |
|
| `↑` / `↓` | Navigate results |
|
||||||
| `Tab` | Cycle filter modes |
|
| `Tab` | Cycle filter tabs |
|
||||||
| `Shift+Tab` | Cycle filter modes (reverse) |
|
| `Shift+Tab` | Cycle filter tabs (reverse) |
|
||||||
| `Ctrl+1` | Toggle Applications filter |
|
| `Ctrl+1..9` | Toggle tab by position |
|
||||||
| `Ctrl+2` | Toggle Commands filter |
|
|
||||||
| `Ctrl+3` | Toggle systemd filter |
|
|
||||||
|
|
||||||
### Search Prefixes
|
### Search Prefixes
|
||||||
|
|
||||||
Filter results by provider using prefixes:
|
|
||||||
|
|
||||||
| Prefix | Provider | Example |
|
| Prefix | Provider | Example |
|
||||||
|--------|----------|---------|
|
|--------|----------|---------|
|
||||||
| `:app` | Applications | `:app firefox` |
|
| `:app` | Applications | `:app firefox` |
|
||||||
| `:cmd` | PATH commands | `:cmd git` |
|
| `:cmd` | PATH commands | `:cmd git` |
|
||||||
| `:sys` | System actions | `:sys shutdown` |
|
| `:sys` | System actions | `:sys shutdown` |
|
||||||
| `:ssh` | SSH hosts | `:ssh server` |
|
| `:ssh` | SSH hosts | `:ssh server` |
|
||||||
| `:clip` | Clipboard history | `:clip password` |
|
| `:clip` | Clipboard | `:clip password` |
|
||||||
| `:bm` | Browser bookmarks | `:bm github` |
|
| `:bm` | Bookmarks | `:bm github` |
|
||||||
| `:emoji` | Emoji picker | `:emoji heart` |
|
| `:emoji` | Emoji | `:emoji heart` |
|
||||||
| `:script` | Custom scripts | `:script backup` |
|
| `:script` | Scripts | `:script backup` |
|
||||||
| `:file` | File search | `:file config.toml` |
|
| `:file` | Files | `:file config` |
|
||||||
| `:calc` | Calculator | `:calc 5+3` |
|
| `:calc` | Calculator | `:calc sqrt(16)` |
|
||||||
| `:web` | Web search | `:web rust docs` |
|
| `:web` | Web search | `:web rust docs` |
|
||||||
| `:uuctl` | systemd services | `:uuctl docker` |
|
| `:uuctl` | systemd | `:uuctl docker` |
|
||||||
|
| `:tag:X` | Filter by tag | `:tag:development` |
|
||||||
|
|
||||||
### Trigger Prefixes
|
### Trigger Prefixes
|
||||||
|
|
||||||
Some providers can be triggered directly without filter mode:
|
|
||||||
|
|
||||||
| Trigger | Provider | Example |
|
| Trigger | Provider | Example |
|
||||||
|---------|----------|---------|
|
|---------|----------|---------|
|
||||||
| `=` | Calculator | `= 5+3` or `=5*2` |
|
| `=` | Calculator | `= 5+3` |
|
||||||
| `calc ` | Calculator | `calc sqrt(16)` |
|
| `calc ` | Calculator | `calc sqrt(16)` |
|
||||||
| `?` | Web search | `? rust programming` |
|
| `?` | Web search | `? rust programming` |
|
||||||
| `web ` | Web search | `web linux tips` |
|
| `web ` | Web search | `web linux tips` |
|
||||||
| `search ` | Web search | `search owlry` |
|
|
||||||
| `/` | File search | `/ .bashrc` |
|
| `/` | File search | `/ .bashrc` |
|
||||||
| `find ` | File search | `find config` |
|
| `find ` | File search | `find config` |
|
||||||
|
|
||||||
## Providers
|
## File Locations
|
||||||
|
|
||||||
### Applications
|
Owlry follows the [XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/latest/):
|
||||||
Searches `.desktop` files from standard XDG directories.
|
|
||||||
|
|
||||||
### Commands
|
| Path | Purpose |
|
||||||
Searches executable files in `$PATH`.
|
|------|---------|
|
||||||
|
| `~/.config/owlry/config.toml` | Main configuration |
|
||||||
### System
|
| `~/.config/owlry/themes/*.css` | Custom themes |
|
||||||
Quick access to system actions:
|
| `~/.config/owlry/style.css` | CSS overrides |
|
||||||
- Shutdown, Reboot, Suspend, Hibernate
|
| `~/.local/share/owlry/scripts/` | User scripts |
|
||||||
- Lock Screen, Log Out
|
| `~/.local/share/owlry/frecency.json` | Usage history |
|
||||||
- **Reboot into BIOS** - Restart directly into UEFI/BIOS setup
|
|
||||||
|
|
||||||
### SSH
|
|
||||||
Parses `~/.ssh/config` and offers quick connections to configured hosts. Opens in your configured terminal.
|
|
||||||
|
|
||||||
### Clipboard (requires cliphist)
|
|
||||||
Search and paste from clipboard history. Requires `cliphist` and `wl-clipboard`:
|
|
||||||
```bash
|
|
||||||
sudo pacman -S cliphist wl-clipboard
|
|
||||||
```
|
|
||||||
|
|
||||||
### Bookmarks
|
|
||||||
Reads bookmarks from Chromium-based browsers:
|
|
||||||
- Chrome, Chromium, Brave, Edge, Vivaldi
|
|
||||||
|
|
||||||
### Emoji
|
|
||||||
Search 300+ emojis by name or keywords. Selected emoji is copied to clipboard via `wl-copy`.
|
|
||||||
|
|
||||||
### Scripts
|
|
||||||
Runs executable scripts from `~/.config/owlry/scripts/`. Create the directory and add your scripts:
|
|
||||||
```bash
|
|
||||||
mkdir -p ~/.config/owlry/scripts
|
|
||||||
echo '#!/bin/bash
|
|
||||||
# My backup script
|
|
||||||
rsync -av ~/Documents /backup/' > ~/.config/owlry/scripts/backup
|
|
||||||
chmod +x ~/.config/owlry/scripts/backup
|
|
||||||
```
|
|
||||||
|
|
||||||
### Calculator
|
|
||||||
Evaluate math expressions with `= expr` or `calc expr`:
|
|
||||||
- Basic: `= 5+3`, `= 10/3`
|
|
||||||
- Functions: `= sqrt(16)`, `= sin(pi/2)`
|
|
||||||
- Constants: `= pi`, `= e`
|
|
||||||
|
|
||||||
### Web Search
|
|
||||||
Search the web with `? query` or `web query`. Configurable search engine:
|
|
||||||
- Google, DuckDuckGo, Bing, Brave, Ecosia, Startpage, SearXNG
|
|
||||||
- Or custom URL with `{query}` placeholder
|
|
||||||
|
|
||||||
### File Search (requires fd or locate)
|
|
||||||
Search files with `/ pattern` or `find pattern`:
|
|
||||||
```bash
|
|
||||||
sudo pacman -S fd # recommended, faster
|
|
||||||
# or
|
|
||||||
sudo pacman -S mlocate && sudo updatedb
|
|
||||||
```
|
|
||||||
|
|
||||||
### systemd User Services
|
|
||||||
Lists and controls user-level systemd services. Select a service to access actions:
|
|
||||||
- Start / Stop / Restart / Reload
|
|
||||||
- Kill (force stop)
|
|
||||||
- Status (opens in terminal)
|
|
||||||
- Journal (live logs in terminal)
|
|
||||||
- Enable / Disable (autostart)
|
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Configuration file: `~/.config/owlry/config.toml`
|
Copy the example files:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Config
|
||||||
mkdir -p ~/.config/owlry
|
mkdir -p ~/.config/owlry
|
||||||
cp /usr/share/doc/owlry/config.example.toml ~/.config/owlry/config.toml
|
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
|
```toml
|
||||||
[general]
|
[general]
|
||||||
show_icons = true
|
show_icons = true
|
||||||
max_results = 10
|
max_results = 10
|
||||||
# terminal_command = "kitty" # Auto-detected if not set
|
tabs = ["app", "cmd", "uuctl"] # Header tabs (Ctrl+1, Ctrl+2, etc.)
|
||||||
# launch_wrapper = "uwsm app --" # Auto-detected for uwsm/hyprland
|
# terminal_command = "kitty" # Auto-detected
|
||||||
|
# launch_wrapper = "uwsm app --" # Auto-detected
|
||||||
|
|
||||||
[appearance]
|
[appearance]
|
||||||
width = 600
|
width = 600
|
||||||
height = 400
|
height = 400
|
||||||
font_size = 14
|
font_size = 14
|
||||||
border_radius = 12
|
border_radius = 12
|
||||||
# theme = "owl" # Optional: "owl" or custom theme name
|
# theme = "owl" # Or: catppuccin-mocha, nord, dracula, etc.
|
||||||
|
|
||||||
[providers]
|
[providers]
|
||||||
applications = true
|
applications = true
|
||||||
@@ -227,7 +163,7 @@ commands = true
|
|||||||
uuctl = true
|
uuctl = true
|
||||||
calculator = true
|
calculator = true
|
||||||
websearch = true
|
websearch = true
|
||||||
search_engine = "duckduckgo" # google, bing, brave, ecosia, startpage, searxng
|
search_engine = "duckduckgo"
|
||||||
system = true
|
system = true
|
||||||
ssh = true
|
ssh = true
|
||||||
clipboard = true
|
clipboard = true
|
||||||
@@ -236,72 +172,83 @@ emoji = true
|
|||||||
scripts = true
|
scripts = true
|
||||||
files = true
|
files = true
|
||||||
frecency = true
|
frecency = true
|
||||||
frecency_weight = 0.3 # 0.0 = disabled, 1.0 = strong boost
|
frecency_weight = 0.3
|
||||||
```
|
```
|
||||||
|
|
||||||
### Default Values
|
### Tab Configuration
|
||||||
|
|
||||||
| Setting | Default |
|
Customize which providers appear as header tabs:
|
||||||
|---------|---------|
|
|
||||||
| `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) |
|
|
||||||
|
|
||||||
### 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 |
|
## Providers
|
||||||
|---------|---------|---------|
|
|
||||||
| uwsm | `uwsm app --` | Proper systemd scope and session management |
|
| Provider | Description | Trigger |
|
||||||
| Hyprland | `hyprctl dispatch exec --` | Native Hyprland window management |
|
|----------|-------------|---------|
|
||||||
| Other | None (direct `sh -c`) | Standard shell execution |
|
| **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
|
## Theming
|
||||||
|
|
||||||
### GTK Theme (Default)
|
|
||||||
|
|
||||||
By default, Owlry inherits colors from your system GTK4 theme (Adwaita, Breeze, etc.).
|
|
||||||
|
|
||||||
### Built-in Themes
|
### 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 |
|
| Theme | Description |
|
||||||
|-------|-------------|
|
|-------|-------------|
|
||||||
| `owl` | Owl-inspired dark theme with amber accents |
|
| `owl` | Dark theme with amber accents |
|
||||||
| `catppuccin-mocha` | Soothing pastel theme |
|
| `catppuccin-mocha` | Soothing pastel |
|
||||||
| `nord` | Arctic, north-bluish palette |
|
| `nord` | Arctic blue palette |
|
||||||
| `rose-pine` | All natural pine, faux fur and soho vibes |
|
| `rose-pine` | Natural pine vibes |
|
||||||
| `dracula` | Dark theme for vampires |
|
| `dracula` | Dark vampire theme |
|
||||||
| `gruvbox-dark` | Retro groove color scheme |
|
| `gruvbox-dark` | Retro groove |
|
||||||
| `tokyo-night` | Lights of Tokyo at night |
|
| `tokyo-night` | Tokyo city lights |
|
||||||
| `solarized-dark` | Precision colors for machines and people |
|
| `solarized-dark` | Precision colors |
|
||||||
| `one-dark` | Atom's iconic One Dark theme |
|
| `one-dark` | Atom's One Dark |
|
||||||
|
|
||||||
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
|
```toml
|
||||||
[appearance]
|
[appearance]
|
||||||
@@ -310,7 +257,7 @@ theme = "catppuccin-mocha"
|
|||||||
|
|
||||||
### Custom Theme
|
### Custom Theme
|
||||||
|
|
||||||
Create a custom theme file at `~/.config/owlry/themes/mytheme.css`:
|
Create `~/.config/owlry/themes/mytheme.css`:
|
||||||
|
|
||||||
```css
|
```css
|
||||||
:root {
|
: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 |
|
| Variable | Description |
|
||||||
|----------|-------------|
|
|----------|-------------|
|
||||||
@@ -333,22 +297,17 @@ Create a custom theme file at `~/.config/owlry/themes/mytheme.css`:
|
|||||||
| `--owlry-border` | Border color |
|
| `--owlry-border` | Border color |
|
||||||
| `--owlry-text` | Primary text |
|
| `--owlry-text` | Primary text |
|
||||||
| `--owlry-text-secondary` | Muted text |
|
| `--owlry-text-secondary` | Muted text |
|
||||||
| `--owlry-accent` | Accent/highlight color |
|
| `--owlry-accent` | Accent color |
|
||||||
| `--owlry-accent-bright` | Bright accent |
|
| `--owlry-accent-bright` | Bright accent |
|
||||||
| `--owlry-font-size` | Base font size |
|
| `--owlry-font-size` | Base font size |
|
||||||
| `--owlry-border-radius` | Border radius |
|
| `--owlry-border-radius` | Corner 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.
|
|
||||||
|
|
||||||
## License
|
## 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
|
## Acknowledgments
|
||||||
|
|
||||||
- [GTK4](https://gtk.org/) - UI toolkit
|
- [GTK4](https://gtk.org/) — UI toolkit
|
||||||
- [gtk4-layer-shell](https://github.com/wmww/gtk4-layer-shell) - Wayland Layer Shell bindings
|
- [gtk4-layer-shell](https://github.com/wmww/gtk4-layer-shell) — Wayland Layer Shell
|
||||||
- [fuzzy-matcher](https://crates.io/crates/fuzzy-matcher) - Fuzzy search algorithm
|
- [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::config::Config;
|
||||||
use crate::data::FrecencyStore;
|
use crate::data::FrecencyStore;
|
||||||
use crate::filter::ProviderFilter;
|
use crate::filter::ProviderFilter;
|
||||||
|
use crate::paths;
|
||||||
use crate::providers::ProviderManager;
|
use crate::providers::ProviderManager;
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
use crate::ui::MainWindow;
|
use crate::ui::MainWindow;
|
||||||
@@ -81,7 +82,7 @@ impl OwlryApp {
|
|||||||
|
|
||||||
// 1. Load base structural CSS (always applied)
|
// 1. Load base structural CSS (always applied)
|
||||||
let base_provider = CssProvider::new();
|
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(
|
gtk4::style_context_add_provider_for_display(
|
||||||
&display,
|
&display,
|
||||||
&base_provider,
|
&base_provider,
|
||||||
@@ -94,14 +95,12 @@ impl OwlryApp {
|
|||||||
let theme_provider = CssProvider::new();
|
let theme_provider = CssProvider::new();
|
||||||
match theme_name.as_str() {
|
match theme_name.as_str() {
|
||||||
"owl" => {
|
"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");
|
debug!("Loaded built-in owl theme");
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
// Check for custom theme in ~/.config/owlry/themes/{name}.css
|
// Check for custom theme in $XDG_CONFIG_HOME/owlry/themes/{name}.css
|
||||||
if let Some(theme_path) = dirs::config_dir()
|
if let Some(theme_path) = paths::theme_file(theme_name) {
|
||||||
.map(|p| p.join("owlry").join("themes").join(format!("{}.css", theme_name)))
|
|
||||||
{
|
|
||||||
if theme_path.exists() {
|
if theme_path.exists() {
|
||||||
theme_provider.load_from_path(&theme_path);
|
theme_provider.load_from_path(&theme_path);
|
||||||
debug!("Loaded custom theme from {:?}", theme_path);
|
debug!("Loaded custom theme from {:?}", theme_path);
|
||||||
@@ -119,7 +118,7 @@ impl OwlryApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. Load user's custom stylesheet if exists
|
// 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() {
|
if custom_path.exists() {
|
||||||
let custom_provider = CssProvider::new();
|
let custom_provider = CssProvider::new();
|
||||||
custom_provider.load_from_path(&custom_path);
|
custom_provider.load_from_path(&custom_path);
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
use log::{debug, info, warn};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use log::{info, warn, debug};
|
|
||||||
|
use crate::paths;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
@@ -20,6 +22,18 @@ pub struct GeneralConfig {
|
|||||||
/// If None or empty, launches directly via sh -c
|
/// If None or empty, launches directly via sh -c
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub launch_wrapper: Option<String>,
|
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
|
/// User-customizable theme colors
|
||||||
@@ -218,6 +232,7 @@ impl Default for Config {
|
|||||||
max_results: 10,
|
max_results: 10,
|
||||||
terminal_command: terminal,
|
terminal_command: terminal,
|
||||||
launch_wrapper: detect_launch_wrapper(),
|
launch_wrapper: detect_launch_wrapper(),
|
||||||
|
tabs: default_tabs(),
|
||||||
},
|
},
|
||||||
appearance: AppearanceConfig {
|
appearance: AppearanceConfig {
|
||||||
width: 600,
|
width: 600,
|
||||||
@@ -250,7 +265,7 @@ impl Default for Config {
|
|||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub fn config_path() -> Option<PathBuf> {
|
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 {
|
pub fn load_or_default() -> Self {
|
||||||
@@ -289,9 +304,7 @@ impl Config {
|
|||||||
pub fn save(&self) -> Result<(), Box<dyn std::error::Error>> {
|
pub fn save(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let path = Self::config_path().ok_or("Could not determine config path")?;
|
let path = Self::config_path().ok_or("Could not determine config path")?;
|
||||||
|
|
||||||
if let Some(parent) = path.parent() {
|
paths::ensure_parent_dir(&path)?;
|
||||||
std::fs::create_dir_all(parent)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let content = toml::to_string_pretty(self)?;
|
let content = toml::to_string_pretty(self)?;
|
||||||
std::fs::write(&path, content)?;
|
std::fs::write(&path, content)?;
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ use serde::{Deserialize, Serialize};
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use crate::paths;
|
||||||
|
|
||||||
/// A single frecency entry tracking launch count and recency
|
/// A single frecency entry tracking launch count and recency
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct FrecencyEntry {
|
pub struct FrecencyEntry {
|
||||||
@@ -56,10 +58,7 @@ impl FrecencyStore {
|
|||||||
|
|
||||||
/// Get the path to the frecency data file
|
/// Get the path to the frecency data file
|
||||||
fn data_path() -> PathBuf {
|
fn data_path() -> PathBuf {
|
||||||
dirs::data_dir()
|
paths::frecency_file().unwrap_or_else(|| PathBuf::from("frecency.json"))
|
||||||
.unwrap_or_else(|| PathBuf::from("."))
|
|
||||||
.join("owlry")
|
|
||||||
.join("frecency.json")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load frecency data from a file
|
/// Load frecency data from a file
|
||||||
@@ -85,10 +84,7 @@ impl FrecencyStore {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure directory exists
|
paths::ensure_parent_dir(&self.path)?;
|
||||||
if let Some(parent) = self.path.parent() {
|
|
||||||
std::fs::create_dir_all(parent)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let content = serde_json::to_string_pretty(&self.data)?;
|
let content = serde_json::to_string_pretty(&self.data)?;
|
||||||
std::fs::write(&self.path, content)?;
|
std::fs::write(&self.path, content)?;
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
#[cfg(feature = "dev-logging")]
|
||||||
|
use log::debug;
|
||||||
|
|
||||||
use crate::config::ProvidersConfig;
|
use crate::config::ProvidersConfig;
|
||||||
use crate::providers::ProviderType;
|
use crate::providers::ProviderType;
|
||||||
|
|
||||||
@@ -14,6 +17,7 @@ pub struct ProviderFilter {
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ParsedQuery {
|
pub struct ParsedQuery {
|
||||||
pub prefix: Option<ProviderType>,
|
pub prefix: Option<ProviderType>,
|
||||||
|
pub tag_filter: Option<String>,
|
||||||
pub query: String,
|
pub query: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,10 +73,15 @@ impl ProviderFilter {
|
|||||||
set
|
set
|
||||||
};
|
};
|
||||||
|
|
||||||
Self {
|
let filter = Self {
|
||||||
enabled,
|
enabled,
|
||||||
active_prefix: None,
|
active_prefix: None,
|
||||||
}
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "dev-logging")]
|
||||||
|
debug!("[Filter] Created with enabled providers: {:?}", filter.enabled);
|
||||||
|
|
||||||
|
filter
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Default filter: apps only
|
/// Default filter: apps only
|
||||||
@@ -92,8 +101,12 @@ impl ProviderFilter {
|
|||||||
if self.enabled.is_empty() {
|
if self.enabled.is_empty() {
|
||||||
self.enabled.insert(ProviderType::Application);
|
self.enabled.insert(ProviderType::Application);
|
||||||
}
|
}
|
||||||
|
#[cfg(feature = "dev-logging")]
|
||||||
|
debug!("[Filter] Toggled OFF {:?}, enabled: {:?}", provider, self.enabled);
|
||||||
} else {
|
} else {
|
||||||
self.enabled.insert(provider);
|
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.)
|
/// Set prefix mode (from :app, :cmd, etc.)
|
||||||
pub fn set_prefix(&mut self, prefix: Option<ProviderType>) {
|
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;
|
self.active_prefix = prefix;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,6 +162,30 @@ impl ProviderFilter {
|
|||||||
pub fn parse_query(query: &str) -> ParsedQuery {
|
pub fn parse_query(query: &str) -> ParsedQuery {
|
||||||
let trimmed = query.trim_start();
|
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)
|
// Check for prefix patterns (with trailing space)
|
||||||
let prefixes = [
|
let prefixes = [
|
||||||
(":app ", ProviderType::Application),
|
(":app ", ProviderType::Application),
|
||||||
@@ -176,8 +217,11 @@ impl ProviderFilter {
|
|||||||
|
|
||||||
for (prefix_str, provider) in prefixes {
|
for (prefix_str, provider) in prefixes {
|
||||||
if let Some(rest) = trimmed.strip_prefix(prefix_str) {
|
if let Some(rest) = trimmed.strip_prefix(prefix_str) {
|
||||||
|
#[cfg(feature = "dev-logging")]
|
||||||
|
debug!("[Filter] parse_query({:?}) -> prefix={:?}, query={:?}", query, provider, rest);
|
||||||
return ParsedQuery {
|
return ParsedQuery {
|
||||||
prefix: Some(provider),
|
prefix: Some(provider),
|
||||||
|
tag_filter: None,
|
||||||
query: rest.to_string(),
|
query: rest.to_string(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -214,17 +258,26 @@ impl ProviderFilter {
|
|||||||
|
|
||||||
for (prefix_str, provider) in partial_prefixes {
|
for (prefix_str, provider) in partial_prefixes {
|
||||||
if trimmed == prefix_str {
|
if trimmed == prefix_str {
|
||||||
|
#[cfg(feature = "dev-logging")]
|
||||||
|
debug!("[Filter] parse_query({:?}) -> partial prefix {:?}", query, provider);
|
||||||
return ParsedQuery {
|
return ParsedQuery {
|
||||||
prefix: Some(provider),
|
prefix: Some(provider),
|
||||||
|
tag_filter: None,
|
||||||
query: String::new(),
|
query: String::new(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ParsedQuery {
|
let result = ParsedQuery {
|
||||||
prefix: None,
|
prefix: None,
|
||||||
|
tag_filter: None,
|
||||||
query: query.to_string(),
|
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)
|
/// Get enabled providers for UI display (sorted)
|
||||||
|
|||||||
18
src/main.rs
18
src/main.rs
@@ -3,6 +3,7 @@ mod cli;
|
|||||||
mod config;
|
mod config;
|
||||||
mod data;
|
mod data;
|
||||||
mod filter;
|
mod filter;
|
||||||
|
mod paths;
|
||||||
mod providers;
|
mod providers;
|
||||||
mod theme;
|
mod theme;
|
||||||
mod ui;
|
mod ui;
|
||||||
@@ -11,11 +12,26 @@ use app::OwlryApp;
|
|||||||
use cli::CliArgs;
|
use cli::CliArgs;
|
||||||
use log::{info, warn};
|
use log::{info, warn};
|
||||||
|
|
||||||
|
#[cfg(feature = "dev-logging")]
|
||||||
|
use log::debug;
|
||||||
|
|
||||||
fn main() {
|
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();
|
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");
|
info!("Starting Owlry launcher");
|
||||||
|
|
||||||
// Diagnostic: log critical environment variables
|
// 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 super::{LaunchItem, Provider, ProviderType};
|
||||||
|
use crate::paths;
|
||||||
use freedesktop_desktop_entry::{DesktopEntry, Iter};
|
use freedesktop_desktop_entry::{DesktopEntry, Iter};
|
||||||
use log::{debug, warn};
|
use log::{debug, warn};
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
/// Clean desktop file field codes from command string.
|
/// Clean desktop file field codes from command string.
|
||||||
/// Removes %f, %F, %u, %U, %d, %D, %n, %N, %i, %c, %k, %v, %m field codes
|
/// 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() }
|
Self { items: Vec::new() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_application_dirs() -> Vec<PathBuf> {
|
fn get_application_dirs() -> Vec<std::path::PathBuf> {
|
||||||
let mut dirs = Vec::new();
|
paths::system_data_dirs()
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,6 +135,12 @@ impl Provider for ApplicationProvider {
|
|||||||
None => continue,
|
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 {
|
let item = LaunchItem {
|
||||||
id: path.to_string_lossy().to_string(),
|
id: path.to_string_lossy().to_string(),
|
||||||
name,
|
name,
|
||||||
@@ -160,6 +149,7 @@ impl Provider for ApplicationProvider {
|
|||||||
provider: ProviderType::Application,
|
provider: ProviderType::Application,
|
||||||
command: run_cmd,
|
command: run_cmd,
|
||||||
terminal: desktop_entry.terminal(),
|
terminal: desktop_entry.terminal(),
|
||||||
|
tags,
|
||||||
};
|
};
|
||||||
|
|
||||||
self.items.push(item);
|
self.items.push(item);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::paths;
|
||||||
use crate::providers::{LaunchItem, Provider, ProviderType};
|
use crate::providers::{LaunchItem, Provider, ProviderType};
|
||||||
use log::{debug, warn};
|
use log::{debug, warn};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
@@ -27,8 +28,8 @@ impl BookmarksProvider {
|
|||||||
fn load_firefox_bookmarks(&mut self) {
|
fn load_firefox_bookmarks(&mut self) {
|
||||||
// Firefox stores bookmarks in places.sqlite
|
// Firefox stores bookmarks in places.sqlite
|
||||||
// The file is locked when Firefox is running, so we read from backup
|
// The file is locked when Firefox is running, so we read from backup
|
||||||
let firefox_dir = match dirs::home_dir() {
|
let firefox_dir = match paths::firefox_dir() {
|
||||||
Some(h) => h.join(".mozilla").join("firefox"),
|
Some(d) => d,
|
||||||
None => return,
|
None => return,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -99,29 +100,10 @@ impl BookmarksProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn load_chrome_bookmarks(&mut self) {
|
fn load_chrome_bookmarks(&mut self) {
|
||||||
// Chrome/Chromium bookmarks are in JSON format
|
// Chrome/Chromium bookmarks are in JSON format (XDG config paths)
|
||||||
let home = match dirs::home_dir() {
|
for path in paths::chromium_bookmark_paths() {
|
||||||
Some(h) => h,
|
|
||||||
None => return,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Try multiple browser paths
|
|
||||||
let bookmark_paths = [
|
|
||||||
// Chrome
|
|
||||||
home.join(".config/google-chrome/Default/Bookmarks"),
|
|
||||||
// Chromium
|
|
||||||
home.join(".config/chromium/Default/Bookmarks"),
|
|
||||||
// Brave
|
|
||||||
home.join(".config/BraveSoftware/Brave-Browser/Default/Bookmarks"),
|
|
||||||
// Edge
|
|
||||||
home.join(".config/microsoft-edge/Default/Bookmarks"),
|
|
||||||
// Vivaldi
|
|
||||||
home.join(".config/vivaldi/Default/Bookmarks"),
|
|
||||||
];
|
|
||||||
|
|
||||||
for path in &bookmark_paths {
|
|
||||||
if path.exists() {
|
if path.exists() {
|
||||||
self.read_chrome_bookmarks(path);
|
self.read_chrome_bookmarks(&path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -175,6 +157,7 @@ impl BookmarksProvider {
|
|||||||
provider: ProviderType::Bookmarks,
|
provider: ProviderType::Bookmarks,
|
||||||
command: format!("xdg-open '{}'", url.replace('\'', "'\\''")),
|
command: format!("xdg-open '{}'", url.replace('\'', "'\\''")),
|
||||||
terminal: false,
|
terminal: false,
|
||||||
|
tags: Vec::new(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ impl CalculatorProvider {
|
|||||||
provider: ProviderType::Calculator,
|
provider: ProviderType::Calculator,
|
||||||
command: format!("echo -n '{}' | wl-copy", result_str),
|
command: format!("echo -n '{}' | wl-copy", result_str),
|
||||||
terminal: false,
|
terminal: false,
|
||||||
|
tags: vec!["math".to_string()],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Err(_) => None,
|
Err(_) => None,
|
||||||
@@ -111,6 +112,7 @@ impl CalculatorProvider {
|
|||||||
// Copy result to clipboard using wl-copy
|
// Copy result to clipboard using wl-copy
|
||||||
command: format!("sh -c 'echo -n \"{}\" | wl-copy'", result_str),
|
command: format!("sh -c 'echo -n \"{}\" | wl-copy'", result_str),
|
||||||
terminal: false,
|
terminal: false,
|
||||||
|
tags: vec!["math".to_string()],
|
||||||
};
|
};
|
||||||
|
|
||||||
debug!("Calculator result: {} = {}", expr, result_str);
|
debug!("Calculator result: {} = {}", expr, result_str);
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ impl ClipboardProvider {
|
|||||||
provider: ProviderType::Clipboard,
|
provider: ProviderType::Clipboard,
|
||||||
command,
|
command,
|
||||||
terminal: false,
|
terminal: false,
|
||||||
|
tags: Vec::new(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ impl Provider for CommandProvider {
|
|||||||
provider: ProviderType::Command,
|
provider: ProviderType::Command,
|
||||||
command: name,
|
command: name,
|
||||||
terminal: false,
|
terminal: false,
|
||||||
|
tags: Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.items.push(item);
|
self.items.push(item);
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ impl Provider for DmenuProvider {
|
|||||||
provider: ProviderType::Dmenu,
|
provider: ProviderType::Dmenu,
|
||||||
command: line.to_string(),
|
command: line.to_string(),
|
||||||
terminal: false,
|
terminal: false,
|
||||||
|
tags: Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.items.push(item);
|
self.items.push(item);
|
||||||
|
|||||||
@@ -406,6 +406,7 @@ impl EmojiProvider {
|
|||||||
// Copy emoji to clipboard using wl-copy
|
// Copy emoji to clipboard using wl-copy
|
||||||
command: format!("printf '%s' '{}' | wl-copy", emoji),
|
command: format!("printf '%s' '{}' | wl-copy", emoji),
|
||||||
terminal: false,
|
terminal: false,
|
||||||
|
tags: Vec::new(), // TODO: Extract category from emoji data
|
||||||
});
|
});
|
||||||
|
|
||||||
// Store the search text for matching (not used directly but could be)
|
// Store the search text for matching (not used directly but could be)
|
||||||
@@ -441,6 +442,10 @@ mod tests {
|
|||||||
let mut provider = EmojiProvider::new();
|
let mut provider = EmojiProvider::new();
|
||||||
provider.refresh();
|
provider.refresh();
|
||||||
assert!(provider.items().len() > 100);
|
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 crate::providers::{LaunchItem, ProviderType};
|
||||||
use log::{debug, warn};
|
use log::{debug, warn};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
@@ -106,7 +107,7 @@ impl FileSearchProvider {
|
|||||||
|
|
||||||
fn search_with_fd(&self, pattern: &str) -> Vec<LaunchItem> {
|
fn search_with_fd(&self, pattern: &str) -> Vec<LaunchItem> {
|
||||||
// fd searches from home directory by default
|
// 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")
|
let output = match Command::new("fd")
|
||||||
.args([
|
.args([
|
||||||
@@ -132,7 +133,7 @@ impl FileSearchProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn search_with_locate(&self, pattern: &str) -> Vec<LaunchItem> {
|
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")
|
let output = match Command::new("locate")
|
||||||
.args([
|
.args([
|
||||||
@@ -190,6 +191,7 @@ impl FileSearchProvider {
|
|||||||
provider: ProviderType::Files,
|
provider: ProviderType::Files,
|
||||||
command,
|
command,
|
||||||
terminal: false,
|
terminal: false,
|
||||||
|
tags: Vec::new(),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ use fuzzy_matcher::FuzzyMatcher;
|
|||||||
use fuzzy_matcher::skim::SkimMatcherV2;
|
use fuzzy_matcher::skim::SkimMatcherV2;
|
||||||
use log::info;
|
use log::info;
|
||||||
|
|
||||||
|
#[cfg(feature = "dev-logging")]
|
||||||
|
use log::debug;
|
||||||
|
|
||||||
use crate::data::FrecencyStore;
|
use crate::data::FrecencyStore;
|
||||||
|
|
||||||
/// Represents a single searchable/launchable item
|
/// Represents a single searchable/launchable item
|
||||||
@@ -43,6 +46,8 @@ pub struct LaunchItem {
|
|||||||
pub provider: ProviderType,
|
pub provider: ProviderType,
|
||||||
pub command: String,
|
pub command: String,
|
||||||
pub terminal: bool,
|
pub terminal: bool,
|
||||||
|
/// Tags/categories for filtering (e.g., from .desktop Categories)
|
||||||
|
pub tags: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
@@ -279,7 +284,7 @@ impl ProviderManager {
|
|||||||
results
|
results
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Search with frecency boosting and calculator support
|
/// Search with frecency boosting, calculator support, and tag filtering
|
||||||
pub fn search_with_frecency(
|
pub fn search_with_frecency(
|
||||||
&mut self,
|
&mut self,
|
||||||
query: &str,
|
query: &str,
|
||||||
@@ -287,13 +292,18 @@ impl ProviderManager {
|
|||||||
filter: &crate::filter::ProviderFilter,
|
filter: &crate::filter::ProviderFilter,
|
||||||
frecency: &FrecencyStore,
|
frecency: &FrecencyStore,
|
||||||
frecency_weight: f64,
|
frecency_weight: f64,
|
||||||
|
tag_filter: Option<&str>,
|
||||||
) -> Vec<(LaunchItem, i64)> {
|
) -> 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();
|
let mut results: Vec<(LaunchItem, i64)> = Vec::new();
|
||||||
|
|
||||||
// Check for calculator query (= or calc prefix)
|
// Check for calculator query (= or calc prefix)
|
||||||
if CalculatorProvider::is_calculator_query(query) {
|
if CalculatorProvider::is_calculator_query(query) {
|
||||||
if let Some(calc_result) = self.calculator.evaluate(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));
|
results.push((calc_result, 10000));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -323,6 +333,8 @@ impl ProviderManager {
|
|||||||
// Check for file search query
|
// Check for file search query
|
||||||
if FileSearchProvider::is_file_query(query) {
|
if FileSearchProvider::is_file_query(query) {
|
||||||
let file_results = self.filesearch.evaluate(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() {
|
for (idx, item) in file_results.into_iter().enumerate() {
|
||||||
// Score decreases for each result to maintain order
|
// Score decreases for each result to maintain order
|
||||||
results.push((item, 8000 - idx as i64));
|
results.push((item, 8000 - idx as i64));
|
||||||
@@ -343,6 +355,14 @@ impl ProviderManager {
|
|||||||
.iter()
|
.iter()
|
||||||
.filter(|p| filter.is_active(p.provider_type()))
|
.filter(|p| filter.is_active(p.provider_type()))
|
||||||
.flat_map(|p| p.items().iter().cloned())
|
.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| {
|
.map(|item| {
|
||||||
let frecency_score = frecency.get_score(&item.id);
|
let frecency_score = frecency.get_score(&item.id);
|
||||||
let boosted = (frecency_score * frecency_weight * 100.0) as i64;
|
let boosted = (frecency_score * frecency_weight * 100.0) as i64;
|
||||||
@@ -355,24 +375,43 @@ impl ProviderManager {
|
|||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Regular search with frecency boost
|
// Regular search with frecency boost and tag matching
|
||||||
let search_results: Vec<(LaunchItem, i64)> = self
|
let search_results: Vec<(LaunchItem, i64)> = self
|
||||||
.providers
|
.providers
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|provider| filter.is_active(provider.provider_type()))
|
.filter(|provider| filter.is_active(provider.provider_type()))
|
||||||
.flat_map(|provider| {
|
.flat_map(|provider| {
|
||||||
provider.items().iter().filter_map(|item| {
|
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 name_score = self.matcher.fuzzy_match(&item.name, query);
|
||||||
let desc_score = item
|
let desc_score = item
|
||||||
.description
|
.description
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|d| self.matcher.fuzzy_match(d, query));
|
.and_then(|d| self.matcher.fuzzy_match(d, query));
|
||||||
|
|
||||||
let base_score = match (name_score, desc_score) {
|
// Also match against tags (lower weight)
|
||||||
(Some(n), Some(d)) => Some(n.max(d)),
|
let tag_score = item
|
||||||
(Some(n), None) => Some(n),
|
.tags
|
||||||
(None, Some(d)) => Some(d / 2),
|
.iter()
|
||||||
(None, None) => None,
|
.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| {
|
base_score.map(|s| {
|
||||||
@@ -387,6 +426,18 @@ impl ProviderManager {
|
|||||||
results.extend(search_results);
|
results.extend(search_results);
|
||||||
results.sort_by(|a, b| b.1.cmp(&a.1));
|
results.sort_by(|a, b| b.1.cmp(&a.1));
|
||||||
results.truncate(max_results);
|
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
|
results
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
use crate::paths;
|
||||||
use crate::providers::{LaunchItem, Provider, ProviderType};
|
use crate::providers::{LaunchItem, Provider, ProviderType};
|
||||||
use log::{debug, warn};
|
use log::{debug, warn};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
use std::path::PathBuf;
|
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 {
|
pub struct ScriptsProvider {
|
||||||
items: Vec<LaunchItem>,
|
items: Vec<LaunchItem>,
|
||||||
}
|
}
|
||||||
@@ -14,14 +15,10 @@ impl ScriptsProvider {
|
|||||||
Self { items: Vec::new() }
|
Self { items: Vec::new() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn scripts_dir() -> Option<PathBuf> {
|
|
||||||
dirs::config_dir().map(|p| p.join("owlry").join("scripts"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_scripts(&mut self) {
|
fn load_scripts(&mut self) {
|
||||||
self.items.clear();
|
self.items.clear();
|
||||||
|
|
||||||
let scripts_dir = match Self::scripts_dir() {
|
let scripts_dir = match paths::scripts_dir() {
|
||||||
Some(p) => p,
|
Some(p) => p,
|
||||||
None => {
|
None => {
|
||||||
debug!("Could not determine scripts directory");
|
debug!("Could not determine scripts directory");
|
||||||
@@ -32,7 +29,7 @@ impl ScriptsProvider {
|
|||||||
if !scripts_dir.exists() {
|
if !scripts_dir.exists() {
|
||||||
debug!("Scripts directory not found at {:?}", scripts_dir);
|
debug!("Scripts directory not found at {:?}", scripts_dir);
|
||||||
// Create the directory for the user
|
// 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);
|
warn!("Failed to create scripts directory: {}", e);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -90,6 +87,7 @@ impl ScriptsProvider {
|
|||||||
provider: ProviderType::Scripts,
|
provider: ProviderType::Scripts,
|
||||||
command: path.to_string_lossy().to_string(),
|
command: path.to_string_lossy().to_string(),
|
||||||
terminal: false,
|
terminal: false,
|
||||||
|
tags: vec!["script".to_string()],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
use crate::paths;
|
||||||
use crate::providers::{LaunchItem, Provider, ProviderType};
|
use crate::providers::{LaunchItem, Provider, ProviderType};
|
||||||
use log::{debug, warn};
|
use log::{debug, warn};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
/// SSH connections provider - parses ~/.ssh/config
|
/// SSH connections provider - parses ~/.ssh/config
|
||||||
pub struct SshProvider {
|
pub struct SshProvider {
|
||||||
@@ -27,8 +27,8 @@ impl SshProvider {
|
|||||||
self.terminal_command = terminal.to_string();
|
self.terminal_command = terminal.to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ssh_config_path() -> Option<PathBuf> {
|
fn ssh_config_path() -> Option<std::path::PathBuf> {
|
||||||
dirs::home_dir().map(|p| p.join(".ssh").join("config"))
|
paths::ssh_config()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_ssh_config(&mut self) {
|
fn parse_ssh_config(&mut self) {
|
||||||
@@ -161,6 +161,7 @@ impl SshProvider {
|
|||||||
provider: ProviderType::Ssh,
|
provider: ProviderType::Ssh,
|
||||||
command,
|
command,
|
||||||
terminal: false, // We're already wrapping in terminal
|
terminal: false, // We're already wrapping in terminal
|
||||||
|
tags: vec!["ssh".to_string()],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ impl SystemProvider {
|
|||||||
provider: ProviderType::System,
|
provider: ProviderType::System,
|
||||||
command: command.to_string(),
|
command: command.to_string(),
|
||||||
terminal: false,
|
terminal: false,
|
||||||
|
tags: vec!["power".to_string(), "system".to_string()],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ impl UuctlProvider {
|
|||||||
provider: ProviderType::Uuctl,
|
provider: ProviderType::Uuctl,
|
||||||
command: format!("systemctl --user restart {}", unit_name),
|
command: format!("systemctl --user restart {}", unit_name),
|
||||||
terminal: false,
|
terminal: false,
|
||||||
|
tags: vec!["systemd".to_string(), "service".to_string()],
|
||||||
});
|
});
|
||||||
|
|
||||||
actions.push(LaunchItem {
|
actions.push(LaunchItem {
|
||||||
@@ -56,6 +57,7 @@ impl UuctlProvider {
|
|||||||
provider: ProviderType::Uuctl,
|
provider: ProviderType::Uuctl,
|
||||||
command: format!("systemctl --user stop {}", unit_name),
|
command: format!("systemctl --user stop {}", unit_name),
|
||||||
terminal: false,
|
terminal: false,
|
||||||
|
tags: vec!["systemd".to_string(), "service".to_string()],
|
||||||
});
|
});
|
||||||
|
|
||||||
actions.push(LaunchItem {
|
actions.push(LaunchItem {
|
||||||
@@ -66,6 +68,7 @@ impl UuctlProvider {
|
|||||||
provider: ProviderType::Uuctl,
|
provider: ProviderType::Uuctl,
|
||||||
command: format!("systemctl --user reload {}", unit_name),
|
command: format!("systemctl --user reload {}", unit_name),
|
||||||
terminal: false,
|
terminal: false,
|
||||||
|
tags: vec!["systemd".to_string(), "service".to_string()],
|
||||||
});
|
});
|
||||||
|
|
||||||
actions.push(LaunchItem {
|
actions.push(LaunchItem {
|
||||||
@@ -76,6 +79,7 @@ impl UuctlProvider {
|
|||||||
provider: ProviderType::Uuctl,
|
provider: ProviderType::Uuctl,
|
||||||
command: format!("systemctl --user kill {}", unit_name),
|
command: format!("systemctl --user kill {}", unit_name),
|
||||||
terminal: false,
|
terminal: false,
|
||||||
|
tags: vec!["systemd".to_string(), "service".to_string()],
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
actions.push(LaunchItem {
|
actions.push(LaunchItem {
|
||||||
@@ -86,6 +90,7 @@ impl UuctlProvider {
|
|||||||
provider: ProviderType::Uuctl,
|
provider: ProviderType::Uuctl,
|
||||||
command: format!("systemctl --user start {}", unit_name),
|
command: format!("systemctl --user start {}", unit_name),
|
||||||
terminal: false,
|
terminal: false,
|
||||||
|
tags: vec!["systemd".to_string(), "service".to_string()],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,6 +103,7 @@ impl UuctlProvider {
|
|||||||
provider: ProviderType::Uuctl,
|
provider: ProviderType::Uuctl,
|
||||||
command: format!("systemctl --user status {}", unit_name),
|
command: format!("systemctl --user status {}", unit_name),
|
||||||
terminal: true,
|
terminal: true,
|
||||||
|
tags: vec!["systemd".to_string(), "service".to_string()],
|
||||||
});
|
});
|
||||||
|
|
||||||
actions.push(LaunchItem {
|
actions.push(LaunchItem {
|
||||||
@@ -108,6 +114,7 @@ impl UuctlProvider {
|
|||||||
provider: ProviderType::Uuctl,
|
provider: ProviderType::Uuctl,
|
||||||
command: format!("journalctl --user -u {} -f", unit_name),
|
command: format!("journalctl --user -u {} -f", unit_name),
|
||||||
terminal: true,
|
terminal: true,
|
||||||
|
tags: vec!["systemd".to_string(), "service".to_string()],
|
||||||
});
|
});
|
||||||
|
|
||||||
actions.push(LaunchItem {
|
actions.push(LaunchItem {
|
||||||
@@ -118,6 +125,7 @@ impl UuctlProvider {
|
|||||||
provider: ProviderType::Uuctl,
|
provider: ProviderType::Uuctl,
|
||||||
command: format!("systemctl --user enable {}", unit_name),
|
command: format!("systemctl --user enable {}", unit_name),
|
||||||
terminal: false,
|
terminal: false,
|
||||||
|
tags: vec!["systemd".to_string(), "service".to_string()],
|
||||||
});
|
});
|
||||||
|
|
||||||
actions.push(LaunchItem {
|
actions.push(LaunchItem {
|
||||||
@@ -128,6 +136,7 @@ impl UuctlProvider {
|
|||||||
provider: ProviderType::Uuctl,
|
provider: ProviderType::Uuctl,
|
||||||
command: format!("systemctl --user disable {}", unit_name),
|
command: format!("systemctl --user disable {}", unit_name),
|
||||||
terminal: false,
|
terminal: false,
|
||||||
|
tags: vec!["systemd".to_string(), "service".to_string()],
|
||||||
});
|
});
|
||||||
|
|
||||||
actions
|
actions
|
||||||
@@ -189,6 +198,7 @@ impl UuctlProvider {
|
|||||||
provider: ProviderType::Uuctl,
|
provider: ProviderType::Uuctl,
|
||||||
command: submenu_data, // Special marker for submenu
|
command: submenu_data, // Special marker for submenu
|
||||||
terminal: false,
|
terminal: false,
|
||||||
|
tags: vec!["systemd".to_string(), "service".to_string()],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -136,6 +136,7 @@ impl WebSearchProvider {
|
|||||||
provider: ProviderType::WebSearch,
|
provider: ProviderType::WebSearch,
|
||||||
command,
|
command,
|
||||||
terminal: false,
|
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);
|
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 */
|
/* Text selection */
|
||||||
selection {
|
selection {
|
||||||
background-color: alpha(var(--owlry-accent, @theme_selected_bg_color), 0.3);
|
background-color: alpha(var(--owlry-accent, @theme_selected_bg_color), 0.3);
|
||||||
@@ -10,6 +10,10 @@ use gtk4::{
|
|||||||
ListBoxRow, Orientation, ScrolledWindow, SelectionMode, ToggleButton,
|
ListBoxRow, Orientation, ScrolledWindow, SelectionMode, ToggleButton,
|
||||||
};
|
};
|
||||||
use log::info;
|
use log::info;
|
||||||
|
|
||||||
|
#[cfg(feature = "dev-logging")]
|
||||||
|
use log::debug;
|
||||||
|
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
@@ -44,6 +48,8 @@ pub struct MainWindow {
|
|||||||
hints_label: Label,
|
hints_label: Label,
|
||||||
filter_buttons: Rc<RefCell<HashMap<ProviderType, ToggleButton>>>,
|
filter_buttons: Rc<RefCell<HashMap<ProviderType, ToggleButton>>>,
|
||||||
submenu_state: Rc<RefCell<SubmenuState>>,
|
submenu_state: Rc<RefCell<SubmenuState>>,
|
||||||
|
/// Parsed tab config (ProviderTypes for cycling)
|
||||||
|
tab_order: Rc<Vec<ProviderType>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MainWindow {
|
impl MainWindow {
|
||||||
@@ -104,8 +110,17 @@ impl MainWindow {
|
|||||||
.build();
|
.build();
|
||||||
filter_tabs.add_css_class("owlry-filter-tabs");
|
filter_tabs.add_css_class("owlry-filter-tabs");
|
||||||
|
|
||||||
// Create toggle buttons for each provider
|
// Parse tabs config to ProviderTypes
|
||||||
let filter_buttons = Self::create_filter_buttons(&filter_tabs, &filter);
|
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));
|
let filter_buttons = Rc::new(RefCell::new(filter_buttons));
|
||||||
|
|
||||||
header_box.append(&mode_label);
|
header_box.append(&mode_label);
|
||||||
@@ -143,7 +158,7 @@ impl MainWindow {
|
|||||||
hints_box.add_css_class("owlry-hints");
|
hints_box.add_css_class("owlry-hints");
|
||||||
|
|
||||||
let hints_label = Label::builder()
|
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)
|
.halign(gtk4::Align::Center)
|
||||||
.hexpand(true)
|
.hexpand(true)
|
||||||
.build();
|
.build();
|
||||||
@@ -173,6 +188,7 @@ impl MainWindow {
|
|||||||
hints_label,
|
hints_label,
|
||||||
filter_buttons,
|
filter_buttons,
|
||||||
submenu_state: Rc::new(RefCell::new(SubmenuState::default())),
|
submenu_state: Rc::new(RefCell::new(SubmenuState::default())),
|
||||||
|
tab_order,
|
||||||
};
|
};
|
||||||
|
|
||||||
main_window.setup_signals();
|
main_window.setup_signals();
|
||||||
@@ -187,38 +203,31 @@ impl MainWindow {
|
|||||||
fn create_filter_buttons(
|
fn create_filter_buttons(
|
||||||
container: &GtkBox,
|
container: &GtkBox,
|
||||||
filter: &Rc<RefCell<ProviderFilter>>,
|
filter: &Rc<RefCell<ProviderFilter>>,
|
||||||
|
tabs: &[String],
|
||||||
) -> HashMap<ProviderType, ToggleButton> {
|
) -> 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();
|
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()
|
let button = ToggleButton::builder()
|
||||||
.label(label)
|
.label(label)
|
||||||
.tooltip_text(shortcut)
|
.tooltip_text(&shortcut)
|
||||||
.active(filter.borrow().is_enabled(provider_type))
|
.active(filter.borrow().is_enabled(provider_type))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
button.add_css_class("owlry-filter-button");
|
button.add_css_class("owlry-filter-button");
|
||||||
let css_class = match provider_type {
|
let css_class = Self::provider_css_class(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",
|
|
||||||
};
|
|
||||||
button.add_css_class(css_class);
|
button.add_css_class(css_class);
|
||||||
|
|
||||||
container.append(&button);
|
container.append(&button);
|
||||||
@@ -228,6 +237,44 @@ impl MainWindow {
|
|||||||
buttons
|
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 {
|
fn build_placeholder(filter: &ProviderFilter) -> String {
|
||||||
let active: Vec<&str> = filter
|
let active: Vec<&str> = filter
|
||||||
.enabled_providers()
|
.enabled_providers()
|
||||||
@@ -252,6 +299,52 @@ impl MainWindow {
|
|||||||
format!("Search {}...", active.join(", "))
|
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
|
/// Scroll the given row into view within the scrolled window
|
||||||
fn scroll_to_row(scrolled: &ScrolledWindow, results_list: &ListBox, row: &ListBoxRow) {
|
fn scroll_to_row(scrolled: &ScrolledWindow, results_list: &ListBox, row: &ListBoxRow) {
|
||||||
let vadj = scrolled.vadjustment();
|
let vadj = scrolled.vadjustment();
|
||||||
@@ -298,6 +391,9 @@ impl MainWindow {
|
|||||||
display_name: &str,
|
display_name: &str,
|
||||||
is_active: bool,
|
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);
|
let actions = UuctlProvider::actions_for_service(unit_name, display_name, is_active);
|
||||||
|
|
||||||
// Save current state
|
// Save current state
|
||||||
@@ -340,7 +436,11 @@ impl MainWindow {
|
|||||||
hints_label: &Label,
|
hints_label: &Label,
|
||||||
search_entry: &Entry,
|
search_entry: &Entry,
|
||||||
filter: &Rc<RefCell<ProviderFilter>>,
|
filter: &Rc<RefCell<ProviderFilter>>,
|
||||||
|
config: &Rc<RefCell<Config>>,
|
||||||
) {
|
) {
|
||||||
|
#[cfg(feature = "dev-logging")]
|
||||||
|
debug!("[UI] Exiting submenu");
|
||||||
|
|
||||||
let saved_search = {
|
let saved_search = {
|
||||||
let mut state = submenu_state.borrow_mut();
|
let mut state = submenu_state.borrow_mut();
|
||||||
state.active = false;
|
state.active = false;
|
||||||
@@ -350,7 +450,7 @@ impl MainWindow {
|
|||||||
|
|
||||||
// Restore UI
|
// Restore UI
|
||||||
mode_label.set_label(filter.borrow().mode_display_name());
|
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_placeholder_text(Some(&Self::build_placeholder(&filter.borrow())));
|
||||||
search_entry.set_text(&saved_search);
|
search_entry.set_text(&saved_search);
|
||||||
|
|
||||||
@@ -450,7 +550,7 @@ impl MainWindow {
|
|||||||
let results: Vec<LaunchItem> = if use_frecency {
|
let results: Vec<LaunchItem> = if use_frecency {
|
||||||
providers
|
providers
|
||||||
.borrow_mut()
|
.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()
|
.into_iter()
|
||||||
.map(|(item, _)| item)
|
.map(|(item, _)| item)
|
||||||
.collect()
|
.collect()
|
||||||
@@ -553,17 +653,21 @@ impl MainWindow {
|
|||||||
let scrolled = self.scrolled.clone();
|
let scrolled = self.scrolled.clone();
|
||||||
let search_entry = self.search_entry.clone();
|
let search_entry = self.search_entry.clone();
|
||||||
let _current_results = self.current_results.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 = self.filter.clone();
|
||||||
let filter_buttons = self.filter_buttons.clone();
|
let filter_buttons = self.filter_buttons.clone();
|
||||||
let mode_label = self.mode_label.clone();
|
let mode_label = self.mode_label.clone();
|
||||||
let hints_label = self.hints_label.clone();
|
let hints_label = self.hints_label.clone();
|
||||||
let submenu_state = self.submenu_state.clone();
|
let submenu_state = self.submenu_state.clone();
|
||||||
|
let tab_order = self.tab_order.clone();
|
||||||
|
|
||||||
key_controller.connect_key_pressed(move |_, key, _, modifiers| {
|
key_controller.connect_key_pressed(move |_, key, _, modifiers| {
|
||||||
let ctrl = modifiers.contains(gtk4::gdk::ModifierType::CONTROL_MASK);
|
let ctrl = modifiers.contains(gtk4::gdk::ModifierType::CONTROL_MASK);
|
||||||
let shift = modifiers.contains(gtk4::gdk::ModifierType::SHIFT_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 {
|
match key {
|
||||||
Key::Escape => {
|
Key::Escape => {
|
||||||
// If in submenu, exit submenu first
|
// If in submenu, exit submenu first
|
||||||
@@ -574,6 +678,7 @@ impl MainWindow {
|
|||||||
&hints_label,
|
&hints_label,
|
||||||
&search_entry,
|
&search_entry,
|
||||||
&filter,
|
&filter,
|
||||||
|
&config,
|
||||||
);
|
);
|
||||||
gtk4::glib::Propagation::Stop
|
gtk4::glib::Propagation::Stop
|
||||||
} else {
|
} else {
|
||||||
@@ -590,6 +695,7 @@ impl MainWindow {
|
|||||||
&hints_label,
|
&hints_label,
|
||||||
&search_entry,
|
&search_entry,
|
||||||
&filter,
|
&filter,
|
||||||
|
&config,
|
||||||
);
|
);
|
||||||
gtk4::glib::Propagation::Stop
|
gtk4::glib::Propagation::Stop
|
||||||
} else {
|
} else {
|
||||||
@@ -631,6 +737,7 @@ impl MainWindow {
|
|||||||
&filter_buttons,
|
&filter_buttons,
|
||||||
&search_entry,
|
&search_entry,
|
||||||
&mode_label,
|
&mode_label,
|
||||||
|
&tab_order,
|
||||||
!shift,
|
!shift,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -643,45 +750,37 @@ impl MainWindow {
|
|||||||
&filter_buttons,
|
&filter_buttons,
|
||||||
&search_entry,
|
&search_entry,
|
||||||
&mode_label,
|
&mode_label,
|
||||||
|
&tab_order,
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
gtk4::glib::Propagation::Stop
|
gtk4::glib::Propagation::Stop
|
||||||
}
|
}
|
||||||
// Ctrl+1/2/3 toggle specific providers (only when not in submenu)
|
// Ctrl+1-9 toggle specific providers based on tab order (only when not in submenu)
|
||||||
Key::_1 if ctrl => {
|
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 {
|
if !submenu_state.borrow().active {
|
||||||
Self::toggle_provider_button(
|
let idx = match key {
|
||||||
ProviderType::Application,
|
Key::_1 => 0,
|
||||||
&filter,
|
Key::_2 => 1,
|
||||||
&filter_buttons,
|
Key::_3 => 2,
|
||||||
&search_entry,
|
Key::_4 => 3,
|
||||||
&mode_label,
|
Key::_5 => 4,
|
||||||
);
|
Key::_6 => 5,
|
||||||
}
|
Key::_7 => 6,
|
||||||
gtk4::glib::Propagation::Stop
|
Key::_8 => 7,
|
||||||
}
|
Key::_9 => 8,
|
||||||
Key::_2 if ctrl => {
|
_ => return gtk4::glib::Propagation::Proceed,
|
||||||
if !submenu_state.borrow().active {
|
};
|
||||||
Self::toggle_provider_button(
|
if let Some(&provider) = tab_order.get(idx) {
|
||||||
ProviderType::Command,
|
Self::toggle_provider_button(
|
||||||
&filter,
|
provider,
|
||||||
&filter_buttons,
|
&filter,
|
||||||
&search_entry,
|
&filter_buttons,
|
||||||
&mode_label,
|
&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,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
gtk4::glib::Propagation::Stop
|
gtk4::glib::Propagation::Stop
|
||||||
}
|
}
|
||||||
@@ -735,24 +834,24 @@ impl MainWindow {
|
|||||||
buttons: &Rc<RefCell<HashMap<ProviderType, ToggleButton>>>,
|
buttons: &Rc<RefCell<HashMap<ProviderType, ToggleButton>>>,
|
||||||
entry: &Entry,
|
entry: &Entry,
|
||||||
mode_label: &Label,
|
mode_label: &Label,
|
||||||
|
tab_order: &[ProviderType],
|
||||||
forward: bool,
|
forward: bool,
|
||||||
) {
|
) {
|
||||||
let order = [
|
if tab_order.is_empty() {
|
||||||
ProviderType::Application,
|
return;
|
||||||
ProviderType::Command,
|
}
|
||||||
ProviderType::Uuctl,
|
|
||||||
];
|
|
||||||
let current = filter.borrow().enabled_providers();
|
let current = filter.borrow().enabled_providers();
|
||||||
|
|
||||||
let next = if current.len() == 1 {
|
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 {
|
if forward {
|
||||||
order[(idx + 1) % order.len()]
|
tab_order[(idx + 1) % tab_order.len()]
|
||||||
} else {
|
} else {
|
||||||
order[(idx + order.len() - 1) % order.len()]
|
tab_order[(idx + tab_order.len() - 1) % tab_order.len()]
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ProviderType::Application
|
tab_order[0]
|
||||||
};
|
};
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -800,7 +899,7 @@ impl MainWindow {
|
|||||||
let results: Vec<LaunchItem> = if use_frecency {
|
let results: Vec<LaunchItem> = if use_frecency {
|
||||||
self.providers
|
self.providers
|
||||||
.borrow_mut()
|
.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()
|
.into_iter()
|
||||||
.map(|(item, _)| item)
|
.map(|(item, _)| item)
|
||||||
.collect()
|
.collect()
|
||||||
@@ -833,10 +932,15 @@ impl MainWindow {
|
|||||||
// Record this launch for frecency tracking
|
// Record this launch for frecency tracking
|
||||||
if config.providers.frecency {
|
if config.providers.frecency {
|
||||||
frecency.borrow_mut().record_launch(&item.id);
|
frecency.borrow_mut().record_launch(&item.id);
|
||||||
|
#[cfg(feature = "dev-logging")]
|
||||||
|
debug!("[UI] Recorded frecency launch for: {}", item.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("Launching: {} ({})", item.name, item.command);
|
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 {
|
let cmd = if item.terminal {
|
||||||
format!("{} -e {}", config.general.terminal_command, item.command)
|
format!("{} -e {}", config.general.terminal_command, item.command)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -82,6 +82,25 @@ impl ResultRow {
|
|||||||
text_box.append(&name_label);
|
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
|
// Provider badge
|
||||||
let badge = Label::builder()
|
let badge = Label::builder()
|
||||||
.label(&item.provider.to_string())
|
.label(&item.provider.to_string())
|
||||||
|
|||||||
Reference in New Issue
Block a user