Compare commits

..

22 Commits

Author SHA1 Message Date
b87447156e chore(owlry-core): bump version to 1.1.2 2026-03-28 11:18:27 +01:00
12d554959a chore(owlry): bump version to 1.0.4 2026-03-28 11:18:26 +01:00
83fa22d84c feat(ui): add result highlighting and remove window shadow
Highlighting:
- Dynamic plugin results (calculator, converter, websearch, filesearch)
  get a subtle accent left-border + background tint when auto-detected
- Exact name matches (case-insensitive) are highlighted the same way
- Exact match on apps gets a higher score boost (50k) than other
  providers (30k), so apps rank first when names match exactly

Shadow:
- Removed hardcoded box-shadow from all theme CSS files
- Added --owlry-shadow variable in base.css (defaults to none)
- Themes can opt into shadow via --owlry-shadow if desired

CSS class: .owlry-result-highlight on ResultRow
2026-03-28 11:17:45 +01:00
ade5d3aeef fix(ui): check icon theme exists on disk before fallback
has_icon() returns true even for broken themes since it checks all
search paths. Instead, verify the theme directory actually exists
in the search path. Falls back to Adwaita only when the configured
theme is genuinely missing from disk.
2026-03-28 11:08:01 +01:00
617c943147 fix: aur-stage glob handling for packages without .install files 2026-03-28 10:51:46 +01:00
1b1e12124b chore(aur): update owlry PKGBUILD to 1.0.3 2026-03-28 10:49:57 +01:00
94556f1fe0 chore(owlry): bump version to 1.0.3 2026-03-28 10:48:55 +01:00
2b98f0651c fix(ui): fall back to Adwaita when system icon theme is broken
If the configured icon theme (e.g. Sweet-Blue) doesn't exist on disk,
GTK falls back to hicolor which has almost no icons. Detect this by
probing for a standard icon, and set Adwaita as the theme — it's
guaranteed to exist as a GTK4 dependency.

This replaces the broken add_search_path("/usr/share/icons/Adwaita")
approach which doesn't work because search paths are scoped to the
active theme name, not the directory name.
2026-03-28 10:48:39 +01:00
75fa770c94 chore: overhaul justfile for current deployment pipeline
Key fixes:
- aur-update-pkg uses correct per-crate tag URLs ({crate}-v{version})
- tag-crate creates per-crate tags instead of generic v{version}
- aur-stage handles embedded .git dirs in AUR subdirectories
- aur-commit stages all AUR files with .git workaround
- release-crate does full pipeline: bump → push → tag → AUR update → publish
- Removed stale release-core recipe that used wrong tag format
2026-03-28 10:31:32 +01:00
c6ba91f06d fix(aur): restrict owlry-core check() to unit tests
The integration test (server_test) loads native plugins which segfault
in the clean makepkg build environment. Use --lib to run only unit tests.
2026-03-28 10:06:07 +01:00
235103e854 fix(aur): correct b2sums for owlry and owlry-core tarballs 2026-03-28 09:54:12 +01:00
8ccaaf28c8 docs: update README for client/daemon package split
- Separate package tables for core, plugins, and meta bundles
- Add owlry-plugin-converter to plugin list and meta-essentials
- Fix build instructions: plugins are in owlry-plugins repo
- Update plugin count to 14
- Remove dead link to gitignored CLAUDE.md
2026-03-28 09:51:29 +01:00
cfd143fe4a chore: track AUR package files (PKGBUILD, .SRCINFO)
The aur/ directory was entirely gitignored, preventing PKGBUILD and
.SRCINFO files from being tracked. Fix .gitignore to only ignore
build artifacts and nested .git dirs, matching the owlry-plugins
repo convention.
2026-03-28 09:34:21 +01:00
10a685c62f chore(owlry): bump version to 1.0.2 2026-03-28 09:16:40 +01:00
34db33c75f chore(owlry-core): bump version to 1.1.1 2026-03-28 09:16:38 +01:00
4bff83b5e6 perf(ui): eliminate redundant results.clone() in search handlers
The full results Vec was cloned into lazy_state.all_results and then
separately consumed for current_results. Now we slice for current_results
and move the original into lazy_state, avoiding one full Vec allocation
per query.
2026-03-28 09:14:11 +01:00
8f7501038d perf(ui): move search IPC off the GTK main thread
Search queries in daemon mode now run on a background thread via
DaemonHandle::query_async(). Results are posted back to the main
thread via glib::spawn_future_local + futures_channel::oneshot.
The GTK event loop is never blocked by IPC, eliminating perceived
input lag.

Local mode (dmenu) continues to use synchronous search since it
has no IPC overhead.
2026-03-28 09:12:20 +01:00
4032205800 perf(ui): defer initial query to after window.present()
update_results('') was called inside MainWindow::new(), blocking the
window from appearing until the daemon responded. Move it to a
glib::idle_add_local_once callback scheduled after present() so the
window renders immediately.
2026-03-28 08:51:33 +01:00
99985c7f3b perf(ui): use tracked count in scroll_to_row instead of child walk
scroll_to_row walked all GTK children via first_child/next_sibling
to count rows. The count is already available in LazyLoadState, so
use that directly. Eliminates O(n) widget traversal per arrow key.
2026-03-28 08:48:52 +01:00
6113217f7b perf(core): sample Utc::now() once per search instead of per-item
get_score() called Utc::now() inside calculate_frecency() for every
item in the search loop. Added get_score_at() that accepts a pre-sampled
timestamp. Eliminates hundreds of unnecessary clock_gettime syscalls
per keystroke.
2026-03-28 08:45:21 +01:00
558d415e12 perf(config): replace which subprocesses with in-process PATH scan
detect_terminal() was spawning up to 17 'which' subprocesses sequentially
on every startup. Replace with std::env::split_paths + is_file() check.
Eliminates 200-500ms of fork+exec overhead on cold cache.
2026-03-28 08:40:22 +01:00
6bde1504b1 chore: add .worktrees/ to gitignore 2026-03-28 08:35:51 +01:00
41 changed files with 1209 additions and 513 deletions

5
.gitignore vendored
View File

@@ -1,8 +1,10 @@
/target
CLAUDE.md
.worktrees/
media.md
# AUR packages (each is its own git repo for aur.archlinux.org)
# Track PKGBUILD and .SRCINFO, ignore build artifacts and sub-repo .git
aur/*/.git/
aur/*/pkg/
aur/*/src/
@@ -10,6 +12,3 @@ aur/*/*.tar.zst
aur/*/*.tar.gz
aur/*/*.tar.xz
aur/*/*.pkg.tar.*
# Keep PKGBUILD and .SRCINFO tracked
.SRCINFO
aur/

5
Cargo.lock generated
View File

@@ -2536,12 +2536,13 @@ dependencies = [
[[package]]
name = "owlry"
version = "1.0.1"
version = "1.0.4"
dependencies = [
"chrono",
"clap",
"dirs",
"env_logger",
"futures-channel",
"glib-build-tools",
"gtk4",
"gtk4-layer-shell",
@@ -2556,7 +2557,7 @@ dependencies = [
[[package]]
name = "owlry-core"
version = "1.1.0"
version = "1.1.2"
dependencies = [
"chrono",
"ctrlc",

View File

@@ -13,7 +13,7 @@ A lightweight, owl-themed application launcher for Wayland, built with GTK4 and
- **Client/daemon architecture** — Instant window appearance, providers stay loaded in memory
- **Modular plugin architecture** — Install only what you need
- **Fuzzy search with tags** — Fast matching across names, descriptions, and category tags
- **13 native plugins** — Calculator, clipboard, emoji, weather, media, and more
- **14 native plugins** — Calculator, clipboard, emoji, weather, media, and more
- **Widget providers** — Weather, media controls, and pomodoro timer at the top of results
- **Config profiles** — Named mode presets for different workflows
- **Filter prefixes** — Scope searches with `:app`, `:cmd`, `:tag:development`, etc.
@@ -35,7 +35,7 @@ yay -S owlry
yay -S owlry-plugin-calculator owlry-plugin-weather
# Or install bundles:
yay -S owlry-meta-essentials # calculator, system, ssh, scripts, bookmarks
yay -S owlry-meta-essentials # calculator, converter, system, ssh, scripts, bookmarks
yay -S owlry-meta-widgets # weather, media, pomodoro
yay -S owlry-meta-tools # clipboard, emoji, websearch, filesearch, systemd
yay -S owlry-meta-full # everything
@@ -47,22 +47,42 @@ yay -S owlry-rune # Rune runtime
### Available Packages
**Core packages** (this repo):
| Package | Description |
|---------|-------------|
| `owlry` | GTK4 UI client |
| `owlry-core` | Headless daemon (plugin host, IPC server) |
| `owlry-lua` | Lua 5.4 script runtime for user plugins |
| `owlry-rune` | Rune script runtime for user plugins |
**Plugin packages** ([owlry-plugins](https://somegit.dev/Owlibou/owlry-plugins) repo):
| Package | Description |
|---------|-------------|
| `owlry` | Core: UI client (`owlry`) and daemon (`owlry-core`) |
| `owlry-plugin-calculator` | Math expressions (`= 5+3`) |
| `owlry-plugin-system` | Shutdown, reboot, suspend, lock |
| `owlry-plugin-ssh` | SSH hosts from `~/.ssh/config` |
| `owlry-plugin-clipboard` | History via cliphist |
| `owlry-plugin-emoji` | 400+ searchable emoji |
| `owlry-plugin-scripts` | User scripts |
| `owlry-plugin-bookmarks` | Firefox, Chrome, Brave, Edge bookmarks |
| `owlry-plugin-websearch` | Web search (`? query`) |
| `owlry-plugin-calculator` | Math expressions (`= 5+3`) |
| `owlry-plugin-clipboard` | History via cliphist |
| `owlry-plugin-converter` | Unit and currency conversion |
| `owlry-plugin-emoji` | 400+ searchable emoji |
| `owlry-plugin-filesearch` | File search (`/ filename`) |
| `owlry-plugin-systemd` | User services with actions |
| `owlry-plugin-weather` | Weather widget |
| `owlry-plugin-media` | MPRIS media controls |
| `owlry-plugin-pomodoro` | Pomodoro timer widget |
| `owlry-plugin-scripts` | User scripts |
| `owlry-plugin-ssh` | SSH hosts from `~/.ssh/config` |
| `owlry-plugin-system` | Shutdown, reboot, suspend, lock |
| `owlry-plugin-systemd` | User services with actions |
| `owlry-plugin-weather` | Weather widget |
| `owlry-plugin-websearch` | Web search (`? query`) |
**Meta bundles:**
| Package | Includes |
|---------|----------|
| `owlry-meta-essentials` | bookmarks, calculator, converter, scripts, ssh, system |
| `owlry-meta-tools` | clipboard, emoji, filesearch, systemd, websearch |
| `owlry-meta-widgets` | media, pomodoro, weather |
| `owlry-meta-full` | All plugins + runtimes |
### Build from Source
@@ -83,22 +103,29 @@ sudo dnf install gtk4-devel gtk4-layer-shell-devel
git clone https://somegit.dev/Owlibou/owlry.git
cd owlry
# Build core only (daemon + UI)
# Build daemon + UI
cargo build --release -p owlry -p owlry-core
# Build specific plugin
cargo build --release -p owlry-plugin-calculator
# Build runtimes (for user plugins)
cargo build --release -p owlry-lua -p owlry-rune
# Build everything
# Build everything in this workspace
cargo build --release --workspace
```
**Plugins** are in a [separate repo](https://somegit.dev/Owlibou/owlry-plugins):
```bash
git clone https://somegit.dev/Owlibou/owlry-plugins.git
cd owlry-plugins
cargo build --release -p owlry-plugin-calculator # or any plugin
```
**Install locally:**
```bash
just install-local
```
This installs both binaries, all plugins, runtimes, and the systemd service files.
This installs the UI, daemon, runtimes, and systemd service files.
## Getting Started
@@ -457,8 +484,6 @@ owlry-core (daemon) owlry (GTK4 UI client)
The daemon keeps providers and plugins loaded in memory, so the UI appears instantly when launched. The UI client is a thin GTK4 layer that sends queries and receives results over the socket.
For detailed architecture information, see [CLAUDE.md](CLAUDE.md).
## License
GNU General Public License v3.0 — see [LICENSE](LICENSE).

13
aur/owlry-core/.SRCINFO Normal file
View File

@@ -0,0 +1,13 @@
pkgbase = owlry-core
pkgdesc = Core daemon for the Owlry application launcher — manages plugins, providers, and search
pkgver = 1.1.1
pkgrel = 1
url = https://somegit.dev/Owlibou/owlry
arch = x86_64
license = GPL-3.0-or-later
makedepends = cargo
depends = gcc-libs
source = owlry-core-1.1.1.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-core-v1.1.1.tar.gz
b2sums = 2924468a55fa62979b324c0c48cff2fa13e348f1d21a6ca5e19596bfbeb88fc932b285586275b219bcd75cacc72c1d1d9fecfe13c90dcbc4b258a193bcda1047
pkgname = owlry-core

41
aur/owlry-core/PKGBUILD Normal file
View File

@@ -0,0 +1,41 @@
# Maintainer: vikingowl <christian@nachtigall.dev>
pkgname=owlry-core
pkgver=1.1.1
pkgrel=1
pkgdesc='Core daemon for the Owlry application launcher — manages plugins, providers, and search'
arch=('x86_64')
url='https://somegit.dev/Owlibou/owlry'
license=('GPL-3.0-or-later')
depends=('gcc-libs')
makedepends=('cargo')
source=("$pkgname-$pkgver.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-core-v$pkgver.tar.gz")
b2sums=('2924468a55fa62979b324c0c48cff2fa13e348f1d21a6ca5e19596bfbeb88fc932b285586275b219bcd75cacc72c1d1d9fecfe13c90dcbc4b258a193bcda1047')
prepare() {
cd "owlry"
export RUSTUP_TOOLCHAIN=stable
cargo fetch --locked --target "$(rustc -vV | sed -n 's/host: //p')"
}
build() {
cd "owlry"
export RUSTUP_TOOLCHAIN=stable
export CARGO_TARGET_DIR=target
cargo build -p owlry-core --frozen --release
}
check() {
cd "owlry"
export RUSTUP_TOOLCHAIN=stable
export CARGO_TARGET_DIR=target
cargo test -p owlry-core --frozen --lib
}
package() {
cd "owlry"
install -Dm755 "target/release/owlry-core" "$pkgdir/usr/bin/owlry-core"
install -Dm644 "systemd/owlry-core.service" "$pkgdir/usr/lib/systemd/user/owlry-core.service"
install -Dm644 "systemd/owlry-core.socket" "$pkgdir/usr/lib/systemd/user/owlry-core.socket"
install -dm755 "$pkgdir/usr/lib/owlry/plugins"
install -dm755 "$pkgdir/usr/lib/owlry/runtimes"
}

13
aur/owlry-lua/.SRCINFO Normal file
View File

@@ -0,0 +1,13 @@
pkgbase = owlry-lua
pkgdesc = Lua scripting runtime for Owlry — enables user-created Lua plugins
pkgver = 1.1.0
pkgrel = 1
url = https://somegit.dev/Owlibou/owlry
arch = x86_64
license = GPL-3.0-or-later
makedepends = cargo
depends = owlry-core
source = owlry-lua-1.1.0.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-lua-v1.1.0.tar.gz
b2sums = d4b200446a31301b1240fd8eede6e10764d7bbc551f2e5549bfdbdcc0fa4a717677c3c2c69778d2dfa336711ac5b74d4987e46082ea589fed961c9d2ff95af76
pkgname = owlry-lua

40
aur/owlry-lua/PKGBUILD Normal file
View File

@@ -0,0 +1,40 @@
# Maintainer: vikingowl <christian@nachtigall.dev>
pkgname=owlry-lua
pkgver=1.1.0
pkgrel=1
pkgdesc="Lua scripting runtime for Owlry — enables user-created Lua plugins"
arch=('x86_64')
url="https://somegit.dev/Owlibou/owlry"
license=('GPL-3.0-or-later')
depends=('owlry-core')
makedepends=('cargo')
source=("$pkgname-$pkgver.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-lua-v$pkgver.tar.gz")
b2sums=('d4b200446a31301b1240fd8eede6e10764d7bbc551f2e5549bfdbdcc0fa4a717677c3c2c69778d2dfa336711ac5b74d4987e46082ea589fed961c9d2ff95af76')
_cratename=owlry-lua
prepare() {
cd "owlry"
export RUSTUP_TOOLCHAIN=stable
cargo fetch --locked --target "$(rustc -vV | sed -n 's/host: //p')"
}
build() {
cd "owlry"
export RUSTUP_TOOLCHAIN=stable
export CARGO_TARGET_DIR=target
cargo build -p $_cratename --frozen --release
}
check() {
cd "owlry"
export RUSTUP_TOOLCHAIN=stable
export CARGO_TARGET_DIR=target
cargo test -p $_cratename --frozen --release
}
package() {
cd "owlry"
install -Dm755 "target/release/lib${_cratename//-/_}.so" \
"$pkgdir/usr/lib/owlry/runtimes/liblua.so"
}

View File

@@ -0,0 +1,19 @@
pkgbase = owlry-meta-essentials
pkgdesc = Essential plugin bundle for Owlry (calculator, converter, system, ssh, scripts, bookmarks)
pkgver = 1.0.0
pkgrel = 2
url = https://somegit.dev/Owlibou/owlry
arch = any
license = GPL-3.0-or-later
depends = owlry
depends = owlry-core
depends = owlry-plugin-bookmarks
depends = owlry-plugin-calculator
depends = owlry-plugin-converter
depends = owlry-plugin-scripts
depends = owlry-plugin-ssh
depends = owlry-plugin-system
conflicts = owlry-essentials
replaces = owlry-essentials
pkgname = owlry-meta-essentials

View File

@@ -0,0 +1,20 @@
# Maintainer: vikingowl <christian@nachtigall.dev>
pkgname=owlry-meta-essentials
pkgver=1.0.0
pkgrel=2
pkgdesc="Essential plugin bundle for Owlry (calculator, converter, system, ssh, scripts, bookmarks)"
arch=('any')
url="https://somegit.dev/Owlibou/owlry"
license=('GPL-3.0-or-later')
depends=(
'owlry'
'owlry-core'
'owlry-plugin-bookmarks'
'owlry-plugin-calculator'
'owlry-plugin-converter'
'owlry-plugin-scripts'
'owlry-plugin-ssh'
'owlry-plugin-system'
)
replaces=('owlry-essentials')
conflicts=('owlry-essentials')

View File

@@ -0,0 +1,29 @@
pkgbase = owlry-meta-full
pkgdesc = Complete Owlry installation with all official plugins and runtimes
pkgver = 1.0.0
pkgrel = 2
url = https://somegit.dev/Owlibou/owlry
arch = any
license = GPL-3.0-or-later
depends = owlry
depends = owlry-core
depends = owlry-plugin-bookmarks
depends = owlry-plugin-calculator
depends = owlry-plugin-converter
depends = owlry-plugin-scripts
depends = owlry-plugin-ssh
depends = owlry-plugin-system
depends = owlry-plugin-clipboard
depends = owlry-plugin-emoji
depends = owlry-plugin-filesearch
depends = owlry-plugin-systemd
depends = owlry-plugin-websearch
depends = owlry-plugin-media
depends = owlry-plugin-pomodoro
depends = owlry-plugin-weather
optdepends = owlry-lua: Lua runtime for custom user plugins
optdepends = owlry-rune: Rune runtime for custom user plugins
conflicts = owlry-full
replaces = owlry-full
pkgname = owlry-meta-full

View File

@@ -0,0 +1,35 @@
# Maintainer: vikingowl <christian@nachtigall.dev>
pkgname=owlry-meta-full
pkgver=1.0.0
pkgrel=2
pkgdesc="Complete Owlry installation with all official plugins and runtimes"
arch=('any')
url="https://somegit.dev/Owlibou/owlry"
license=('GPL-3.0-or-later')
depends=(
'owlry'
'owlry-core'
# Essential plugins
'owlry-plugin-bookmarks'
'owlry-plugin-calculator'
'owlry-plugin-converter'
'owlry-plugin-scripts'
'owlry-plugin-ssh'
'owlry-plugin-system'
# Tool plugins
'owlry-plugin-clipboard'
'owlry-plugin-emoji'
'owlry-plugin-filesearch'
'owlry-plugin-systemd'
'owlry-plugin-websearch'
# Widget plugins
'owlry-plugin-media'
'owlry-plugin-pomodoro'
'owlry-plugin-weather'
)
optdepends=(
'owlry-lua: Lua runtime for custom user plugins'
'owlry-rune: Rune runtime for custom user plugins'
)
replaces=('owlry-full')
conflicts=('owlry-full')

View File

@@ -0,0 +1,18 @@
pkgbase = owlry-meta-tools
pkgdesc = Tool plugin bundle for Owlry (clipboard, emoji, web search, file search, systemd)
pkgver = 1.0.0
pkgrel = 1
url = https://somegit.dev/Owlibou/owlry
arch = any
license = GPL-3.0-or-later
depends = owlry
depends = owlry-core
depends = owlry-plugin-clipboard
depends = owlry-plugin-emoji
depends = owlry-plugin-filesearch
depends = owlry-plugin-systemd
depends = owlry-plugin-websearch
conflicts = owlry-tools
replaces = owlry-tools
pkgname = owlry-meta-tools

View File

@@ -0,0 +1,19 @@
# Maintainer: vikingowl <christian@nachtigall.dev>
pkgname=owlry-meta-tools
pkgver=1.0.0
pkgrel=1
pkgdesc="Tool plugin bundle for Owlry (clipboard, emoji, web search, file search, systemd)"
arch=('any')
url="https://somegit.dev/Owlibou/owlry"
license=('GPL-3.0-or-later')
depends=(
'owlry'
'owlry-core'
'owlry-plugin-clipboard'
'owlry-plugin-emoji'
'owlry-plugin-filesearch'
'owlry-plugin-systemd'
'owlry-plugin-websearch'
)
replaces=('owlry-tools')
conflicts=('owlry-tools')

View File

@@ -0,0 +1,16 @@
pkgbase = owlry-meta-widgets
pkgdesc = Widget plugin bundle for Owlry (weather, media controls, pomodoro timer)
pkgver = 1.0.0
pkgrel = 1
url = https://somegit.dev/Owlibou/owlry
arch = any
license = GPL-3.0-or-later
depends = owlry
depends = owlry-core
depends = owlry-plugin-media
depends = owlry-plugin-pomodoro
depends = owlry-plugin-weather
conflicts = owlry-widgets
replaces = owlry-widgets
pkgname = owlry-meta-widgets

View File

@@ -0,0 +1,17 @@
# Maintainer: vikingowl <christian@nachtigall.dev>
pkgname=owlry-meta-widgets
pkgver=1.0.0
pkgrel=1
pkgdesc="Widget plugin bundle for Owlry (weather, media controls, pomodoro timer)"
arch=('any')
url="https://somegit.dev/Owlibou/owlry"
license=('GPL-3.0-or-later')
depends=(
'owlry'
'owlry-core'
'owlry-plugin-media'
'owlry-plugin-pomodoro'
'owlry-plugin-weather'
)
replaces=('owlry-widgets')
conflicts=('owlry-widgets')

13
aur/owlry-rune/.SRCINFO Normal file
View File

@@ -0,0 +1,13 @@
pkgbase = owlry-rune
pkgdesc = Rune scripting runtime for Owlry — enables user-created Rune plugins
pkgver = 1.1.0
pkgrel = 1
url = https://somegit.dev/Owlibou/owlry
arch = x86_64
license = GPL-3.0-or-later
makedepends = cargo
depends = owlry-core
source = owlry-rune-1.1.0.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-rune-v1.1.0.tar.gz
b2sums = d4b200446a31301b1240fd8eede6e10764d7bbc551f2e5549bfdbdcc0fa4a717677c3c2c69778d2dfa336711ac5b74d4987e46082ea589fed961c9d2ff95af76
pkgname = owlry-rune

40
aur/owlry-rune/PKGBUILD Normal file
View File

@@ -0,0 +1,40 @@
# Maintainer: vikingowl <christian@nachtigall.dev>
pkgname=owlry-rune
pkgver=1.1.0
pkgrel=1
pkgdesc="Rune scripting runtime for Owlry — enables user-created Rune plugins"
arch=('x86_64')
url="https://somegit.dev/Owlibou/owlry"
license=('GPL-3.0-or-later')
depends=('owlry-core')
makedepends=('cargo')
source=("$pkgname-$pkgver.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-rune-v$pkgver.tar.gz")
b2sums=('d4b200446a31301b1240fd8eede6e10764d7bbc551f2e5549bfdbdcc0fa4a717677c3c2c69778d2dfa336711ac5b74d4987e46082ea589fed961c9d2ff95af76')
_cratename=owlry-rune
prepare() {
cd "owlry"
export RUSTUP_TOOLCHAIN=stable
cargo fetch --locked --target "$(rustc -vV | sed -n 's/host: //p')"
}
build() {
cd "owlry"
export RUSTUP_TOOLCHAIN=stable
export CARGO_TARGET_DIR=target
cargo build -p $_cratename --frozen --release
}
check() {
cd "owlry"
export RUSTUP_TOOLCHAIN=stable
export CARGO_TARGET_DIR=target
cargo test -p $_cratename --frozen --release
}
package() {
cd "owlry"
install -Dm755 "target/release/lib${_cratename//-/_}.so" \
"$pkgdir/usr/lib/owlry/runtimes/librune.so"
}

34
aur/owlry/.SRCINFO Normal file
View File

@@ -0,0 +1,34 @@
pkgbase = owlry
pkgdesc = Lightweight Wayland application launcher with plugin support
pkgver = 1.0.3
pkgrel = 1
url = https://somegit.dev/Owlibou/owlry
arch = x86_64
license = GPL-3.0-or-later
makedepends = cargo
depends = owlry-core
depends = gcc-libs
depends = gtk4
depends = gtk4-layer-shell
optdepends = cliphist: clipboard provider support
optdepends = wl-clipboard: clipboard and emoji copy support
optdepends = fd: fast file search
optdepends = owlry-plugin-calculator: calculator provider
optdepends = owlry-plugin-clipboard: clipboard provider
optdepends = owlry-plugin-emoji: emoji picker
optdepends = owlry-plugin-bookmarks: browser bookmarks
optdepends = owlry-plugin-ssh: SSH host launcher
optdepends = owlry-plugin-scripts: custom scripts provider
optdepends = owlry-plugin-system: system actions (shutdown, reboot, etc.)
optdepends = owlry-plugin-websearch: web search provider
optdepends = owlry-plugin-filesearch: file search provider
optdepends = owlry-plugin-systemd: systemd service management
optdepends = owlry-plugin-weather: weather widget
optdepends = owlry-plugin-media: media player controls
optdepends = owlry-plugin-pomodoro: pomodoro timer widget
optdepends = owlry-lua: Lua runtime for user plugins
optdepends = owlry-rune: Rune runtime for user plugins
source = owlry-1.0.3.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-v1.0.3.tar.gz
b2sums = e20a227d0d5fd957155f7edbe5eadb24acf22b1f89df0620a619770f20f568621350a09973fe4d06aa0e4302e2929d4d770ad06e3c20c619af04eba17ab796de
pkgname = owlry

76
aur/owlry/PKGBUILD Normal file
View File

@@ -0,0 +1,76 @@
# Maintainer: vikingowl <christian@nachtigall.dev>
pkgname=owlry
pkgver=1.0.3
pkgrel=1
pkgdesc="Lightweight Wayland application launcher with plugin support"
arch=('x86_64')
url="https://somegit.dev/Owlibou/owlry"
license=('GPL-3.0-or-later')
depends=('owlry-core' 'gcc-libs' 'gtk4' 'gtk4-layer-shell')
makedepends=('cargo')
optdepends=(
'cliphist: clipboard provider support'
'wl-clipboard: clipboard and emoji copy support'
'fd: fast file search'
'owlry-plugin-calculator: calculator provider'
'owlry-plugin-clipboard: clipboard provider'
'owlry-plugin-emoji: emoji picker'
'owlry-plugin-bookmarks: browser bookmarks'
'owlry-plugin-ssh: SSH host launcher'
'owlry-plugin-scripts: custom scripts provider'
'owlry-plugin-system: system actions (shutdown, reboot, etc.)'
'owlry-plugin-websearch: web search provider'
'owlry-plugin-filesearch: file search provider'
'owlry-plugin-systemd: systemd service management'
'owlry-plugin-weather: weather widget'
'owlry-plugin-media: media player controls'
'owlry-plugin-pomodoro: pomodoro timer widget'
'owlry-lua: Lua runtime for user plugins'
'owlry-rune: Rune runtime for user plugins'
)
source=("$pkgname-$pkgver.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-v$pkgver.tar.gz")
b2sums=('e20a227d0d5fd957155f7edbe5eadb24acf22b1f89df0620a619770f20f568621350a09973fe4d06aa0e4302e2929d4d770ad06e3c20c619af04eba17ab796de')
prepare() {
cd "owlry"
export RUSTUP_TOOLCHAIN=stable
cargo fetch --locked --target "$(rustc -vV | sed -n 's/host: //p')"
}
build() {
cd "owlry"
export RUSTUP_TOOLCHAIN=stable
export CARGO_TARGET_DIR=target
# Build only the core binary without embedded Lua (Lua runtime is separate package)
cargo build -p owlry --frozen --release --no-default-features
}
check() {
cd "owlry"
export RUSTUP_TOOLCHAIN=stable
export CARGO_TARGET_DIR=target
cargo test -p owlry --frozen --no-default-features
}
package() {
cd "owlry"
# Core binary
install -Dm755 "target/release/$pkgname" "$pkgdir/usr/bin/$pkgname"
# Documentation
install -Dm644 README.md "$pkgdir/usr/share/doc/$pkgname/README.md"
# Example configuration files
install -Dm644 data/config.example.toml "$pkgdir/usr/share/doc/$pkgname/config.example.toml"
install -Dm644 data/style.example.css "$pkgdir/usr/share/doc/$pkgname/style.example.css"
install -Dm755 data/scripts/example.sh "$pkgdir/usr/share/doc/$pkgname/scripts/example.sh"
# Install themes
install -d "$pkgdir/usr/share/$pkgname/themes"
install -Dm644 data/themes/*.css "$pkgdir/usr/share/$pkgname/themes/"
# Example plugins (for user plugin development)
install -d "$pkgdir/usr/share/$pkgname/examples/plugins"
cp -r examples/plugins/* "$pkgdir/usr/share/$pkgname/examples/plugins/"
}

View File

@@ -1,6 +1,6 @@
[package]
name = "owlry-core"
version = "1.1.0"
version = "1.1.2"
edition.workspace = true
rust-version.workspace = true
license.workspace = true

View File

@@ -2,7 +2,6 @@ use log::{debug, info, warn};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use std::process::Command;
use crate::paths;
@@ -522,12 +521,15 @@ fn detect_de_terminal() -> Option<String> {
None
}
/// Check if a command exists in PATH
/// Check if a command exists in PATH (in-process, no subprocess spawning)
fn command_exists(cmd: &str) -> bool {
Command::new("which")
.arg(cmd)
.output()
.map(|o| o.status.success())
std::env::var_os("PATH")
.map(|paths| {
std::env::split_paths(&paths).any(|dir| {
let full = dir.join(cmd);
full.is_file()
})
})
.unwrap_or(false)
}
@@ -591,3 +593,17 @@ impl Config {
Ok(())
}
}
#[cfg(test)]
mod tests {
#[test]
fn command_exists_finds_sh() {
// /bin/sh exists on every Unix system
assert!(super::command_exists("sh"));
}
#[test]
fn command_exists_rejects_nonexistent() {
assert!(!super::command_exists("owlry_nonexistent_binary_abc123"));
}
}

View File

@@ -131,23 +131,36 @@ impl FrecencyStore {
}
}
/// Calculate frecency score using a pre-sampled timestamp.
/// Use this in hot loops to avoid repeated Utc::now() syscalls.
pub fn get_score_at(&self, item_id: &str, now: DateTime<Utc>) -> f64 {
match self.data.entries.get(item_id) {
Some(entry) => Self::calculate_frecency_at(entry.launch_count, entry.last_launch, now),
None => 0.0,
}
}
/// Calculate frecency using Firefox-style algorithm
fn calculate_frecency(launch_count: u32, last_launch: DateTime<Utc>) -> f64 {
let now = Utc::now();
Self::calculate_frecency_at(launch_count, last_launch, now)
}
/// Calculate frecency using a caller-provided timestamp.
fn calculate_frecency_at(launch_count: u32, last_launch: DateTime<Utc>, now: DateTime<Utc>) -> f64 {
let age = now.signed_duration_since(last_launch);
let age_days = age.num_hours() as f64 / 24.0;
// Recency weight based on how recently the item was used
let recency_weight = if age_days < 1.0 {
100.0 // Today
100.0
} else if age_days < 7.0 {
70.0 // This week
70.0
} else if age_days < 30.0 {
50.0 // This month
50.0
} else if age_days < 90.0 {
30.0 // This quarter
30.0
} else {
10.0 // Older
10.0
};
launch_count as f64 * recency_weight
@@ -206,6 +219,32 @@ mod tests {
assert!(score_month < score_week);
}
#[test]
fn get_score_at_matches_get_score() {
let mut store = FrecencyStore {
data: FrecencyData {
version: 1,
entries: HashMap::new(),
},
path: PathBuf::from("/dev/null"),
dirty: false,
};
store.data.entries.insert(
"test".to_string(),
FrecencyEntry {
launch_count: 5,
last_launch: Utc::now(),
},
);
let now = Utc::now();
let score_at = store.get_score_at("test", now);
let score = store.get_score("test");
// Both should be very close (same timestamp, within rounding)
assert!((score_at - score).abs() < 1.0);
}
#[test]
fn test_launch_count_matters() {
let now = Utc::now();

View File

@@ -16,6 +16,7 @@ pub use command::CommandProvider;
// Re-export native provider for plugin loading
pub use native_provider::NativeProvider;
use chrono::Utc;
use fuzzy_matcher::FuzzyMatcher;
use fuzzy_matcher::skim::SkimMatcherV2;
use log::info;
@@ -570,6 +571,7 @@ impl ProviderManager {
query, max_results, frecency_weight
);
let now = Utc::now();
let mut results: Vec<(LaunchItem, i64)> = Vec::new();
// Add widget items first (highest priority) - only when:
@@ -633,7 +635,7 @@ impl ProviderManager {
}
})
.map(|item| {
let frecency_score = frecency.get_score(&item.id);
let frecency_score = frecency.get_score_at(&item.id, now);
let boosted = (frecency_score * frecency_weight * 100.0) as i64;
(item, boosted)
})
@@ -682,9 +684,20 @@ impl ProviderManager {
};
base_score.map(|s| {
let frecency_score = frecency.get_score(&item.id);
let frecency_score = frecency.get_score_at(&item.id, now);
let frecency_boost = (frecency_score * frecency_weight * 10.0) as i64;
(item.clone(), s + frecency_boost)
// Exact name match bonus — apps get a higher boost
let exact_match_boost = if item.name.eq_ignore_ascii_case(query) {
match &item.provider {
ProviderType::Application => 50_000,
_ => 30_000,
}
} else {
0
};
(item.clone(), s + frecency_boost + exact_match_boost)
})
};

View File

@@ -1,6 +1,6 @@
[package]
name = "owlry"
version = "1.0.1"
version = "1.0.4"
edition = "2024"
rust-version = "1.90"
description = "A lightweight, owl-themed application launcher for Wayland"
@@ -46,6 +46,9 @@ dirs = "5"
# Semantic versioning (needed by plugin commands)
semver = "1"
# Async oneshot channel (background thread -> main loop)
futures-channel = "0.3"
[build-dependencies]
# GResource compilation for bundled icons
glib-build-tools = "0.20"

View File

@@ -69,7 +69,7 @@ impl OwlryApp {
match CoreClient::connect_or_start() {
Ok(client) => {
info!("Connected to owlry-core daemon");
SearchBackend::Daemon(client)
SearchBackend::Daemon(crate::backend::DaemonHandle::new(client))
}
Err(e) => {
warn!(
@@ -135,6 +135,9 @@ impl OwlryApp {
Self::load_css(&config.borrow());
window.present();
// Populate results AFTER present() so the window appears immediately
window.schedule_initial_results();
}
/// Create a local backend as fallback when daemon is unavailable.
@@ -182,16 +185,25 @@ impl OwlryApp {
}
fn setup_icon_theme() {
// Ensure we have icon fallbacks for weather/media icons
// These may not exist in all icon themes
if let Some(display) = gtk4::gdk::Display::default() {
let icon_theme = gtk4::IconTheme::for_display(&display);
// Add Adwaita as fallback search path (has weather and media icons)
icon_theme.add_search_path("/usr/share/icons/Adwaita");
icon_theme.add_search_path("/usr/share/icons/breeze");
// If the system icon theme doesn't exist on disk (e.g., set in
// gsettings but not installed), GTK falls back to hicolor which
// has almost no icons. Detect this and use Adwaita instead.
let theme_name = icon_theme.theme_name();
let theme_exists = icon_theme
.search_path()
.iter()
.any(|p| p.join(theme_name.as_str()).is_dir());
debug!("Icon theme search paths configured with Adwaita/breeze fallbacks");
if !theme_exists && theme_name != "hicolor" && theme_name != "Adwaita" {
info!(
"Icon theme '{}' not found on disk, falling back to Adwaita",
theme_name
);
icon_theme.set_theme_name(Some("Adwaita"));
}
}
}

View File

@@ -10,12 +10,87 @@ use owlry_core::data::FrecencyStore;
use owlry_core::filter::ProviderFilter;
use owlry_core::ipc::ResultItem;
use owlry_core::providers::{LaunchItem, ProviderManager, ProviderType};
use std::sync::{Arc, Mutex};
/// Parameters needed to run a search query on a background thread.
pub struct QueryParams {
pub query: String,
#[allow(dead_code)]
pub max_results: usize,
pub modes: Option<Vec<String>>,
pub tag_filter: Option<String>,
}
/// Result of an async search, sent back to the main thread.
pub struct QueryResult {
#[allow(dead_code)]
pub query: String,
pub items: Vec<LaunchItem>,
}
/// Thread-safe handle to the daemon IPC connection.
pub struct DaemonHandle {
pub(crate) client: Arc<Mutex<CoreClient>>,
}
impl DaemonHandle {
pub fn new(client: CoreClient) -> Self {
Self {
client: Arc::new(Mutex::new(client)),
}
}
/// Dispatch an IPC query on a background thread.
///
/// Returns a `futures_channel::oneshot::Receiver` that resolves with
/// the `QueryResult` once the background thread completes IPC. The
/// caller should `.await` it inside `glib::spawn_future_local` to
/// process results on the GTK main thread without `Send` constraints.
pub fn query_async(
&self,
params: QueryParams,
) -> futures_channel::oneshot::Receiver<QueryResult> {
let (tx, rx) = futures_channel::oneshot::channel();
let client = Arc::clone(&self.client);
let query_for_result = params.query.clone();
std::thread::spawn(move || {
let items = match client.lock() {
Ok(mut c) => {
let effective_query = if let Some(ref tag) = params.tag_filter {
format!(":tag:{} {}", tag, params.query)
} else {
params.query
};
match c.query(&effective_query, params.modes) {
Ok(items) => items.into_iter().map(result_to_launch_item).collect(),
Err(e) => {
warn!("IPC query failed: {}", e);
Vec::new()
}
}
}
Err(e) => {
warn!("Failed to lock daemon client: {}", e);
Vec::new()
}
};
let _ = tx.send(QueryResult {
query: query_for_result,
items,
});
});
rx
}
}
/// Backend for search operations. Wraps either an IPC client (daemon mode)
/// or a local ProviderManager (dmenu mode).
pub enum SearchBackend {
/// IPC client connected to owlry-core daemon
Daemon(CoreClient),
Daemon(DaemonHandle),
/// Direct local provider manager (dmenu mode only)
Local {
providers: Box<ProviderManager>,
@@ -24,6 +99,22 @@ pub enum SearchBackend {
}
impl SearchBackend {
/// Build the modes parameter from a ProviderFilter.
/// When accept_all, returns None so the daemon doesn't restrict to a specific set
/// (otherwise dynamically loaded plugin types would be filtered out).
fn build_modes_param(filter: &ProviderFilter) -> Option<Vec<String>> {
if filter.is_accept_all() {
None
} else {
let modes: Vec<String> = filter
.enabled_providers()
.iter()
.map(|p| p.to_string())
.collect();
if modes.is_empty() { None } else { Some(modes) }
}
}
/// Search for items matching the query.
///
/// In daemon mode, sends query over IPC. The modes list is derived from
@@ -38,24 +129,18 @@ impl SearchBackend {
config: &Config,
) -> Vec<LaunchItem> {
match self {
SearchBackend::Daemon(client) => {
// When accept_all, send None so daemon doesn't restrict to a specific set
// (otherwise dynamically loaded plugin types would be filtered out)
let modes_param = if filter.is_accept_all() {
None
} else {
let modes: Vec<String> = filter
.enabled_providers()
.iter()
.map(|p| p.to_string())
.collect();
if modes.is_empty() { None } else { Some(modes) }
};
match client.query(query, modes_param) {
Ok(items) => items.into_iter().map(result_to_launch_item).collect(),
SearchBackend::Daemon(handle) => {
let modes_param = Self::build_modes_param(filter);
match handle.client.lock() {
Ok(mut client) => match client.query(query, modes_param) {
Ok(items) => items.into_iter().map(result_to_launch_item).collect(),
Err(e) => {
warn!("IPC query failed: {}", e);
Vec::new()
}
},
Err(e) => {
warn!("IPC query failed: {}", e);
warn!("Failed to lock daemon client: {}", e);
Vec::new()
}
}
@@ -101,32 +186,24 @@ impl SearchBackend {
tag_filter: Option<&str>,
) -> Vec<LaunchItem> {
match self {
SearchBackend::Daemon(client) => {
// Daemon doesn't support tag filtering in IPC yet — pass query as-is.
// If there's a tag filter, prepend it so the daemon can handle it.
SearchBackend::Daemon(handle) => {
let effective_query = if let Some(tag) = tag_filter {
format!(":tag:{} {}", tag, query)
} else {
query.to_string()
};
// When accept_all, send None so daemon doesn't restrict to a specific set
// (otherwise dynamically loaded plugin types would be filtered out)
let modes_param = if filter.is_accept_all() {
None
} else {
let modes: Vec<String> = filter
.enabled_providers()
.iter()
.map(|p| p.to_string())
.collect();
if modes.is_empty() { None } else { Some(modes) }
};
match client.query(&effective_query, modes_param) {
Ok(items) => items.into_iter().map(result_to_launch_item).collect(),
let modes_param = Self::build_modes_param(filter);
match handle.client.lock() {
Ok(mut client) => match client.query(&effective_query, modes_param) {
Ok(items) => items.into_iter().map(result_to_launch_item).collect(),
Err(e) => {
warn!("IPC query failed: {}", e);
Vec::new()
}
},
Err(e) => {
warn!("IPC query failed: {}", e);
warn!("Failed to lock daemon client: {}", e);
Vec::new()
}
}
@@ -162,13 +239,43 @@ impl SearchBackend {
}
}
/// Dispatch async search (daemon mode only).
/// Returns `Some(Receiver)` if dispatched, `None` for local mode.
pub fn query_async(
&self,
query: &str,
max_results: usize,
filter: &ProviderFilter,
_config: &Config,
tag_filter: Option<&str>,
) -> Option<futures_channel::oneshot::Receiver<QueryResult>> {
match self {
SearchBackend::Daemon(handle) => {
let params = QueryParams {
query: query.to_string(),
max_results,
modes: Self::build_modes_param(filter),
tag_filter: tag_filter.map(|s| s.to_string()),
};
Some(handle.query_async(params))
}
SearchBackend::Local { .. } => None,
}
}
/// Execute a plugin action command. Returns true if handled.
pub fn execute_plugin_action(&mut self, command: &str) -> bool {
match self {
SearchBackend::Daemon(client) => match client.plugin_action(command) {
Ok(handled) => handled,
SearchBackend::Daemon(handle) => match handle.client.lock() {
Ok(mut client) => match client.plugin_action(command) {
Ok(handled) => handled,
Err(e) => {
warn!("IPC plugin_action failed: {}", e);
false
}
},
Err(e) => {
warn!("IPC plugin_action failed: {}", e);
warn!("Failed to lock daemon client: {}", e);
false
}
},
@@ -185,15 +292,21 @@ impl SearchBackend {
display_name: &str,
) -> Option<(String, Vec<LaunchItem>)> {
match self {
SearchBackend::Daemon(client) => match client.submenu(plugin_id, data) {
Ok(items) if !items.is_empty() => {
let actions: Vec<LaunchItem> =
items.into_iter().map(result_to_launch_item).collect();
Some((display_name.to_string(), actions))
}
Ok(_) => None,
SearchBackend::Daemon(handle) => match handle.client.lock() {
Ok(mut client) => match client.submenu(plugin_id, data) {
Ok(items) if !items.is_empty() => {
let actions: Vec<LaunchItem> =
items.into_iter().map(result_to_launch_item).collect();
Some((display_name.to_string(), actions))
}
Ok(_) => None,
Err(e) => {
warn!("IPC submenu query failed: {}", e);
None
}
},
Err(e) => {
warn!("IPC submenu query failed: {}", e);
warn!("Failed to lock daemon client: {}", e);
None
}
},
@@ -206,9 +319,13 @@ impl SearchBackend {
/// Record a launch event for frecency tracking.
pub fn record_launch(&mut self, item_id: &str, provider: &str) {
match self {
SearchBackend::Daemon(client) => {
if let Err(e) = client.launch(item_id, provider) {
warn!("IPC launch notification failed: {}", e);
SearchBackend::Daemon(handle) => {
if let Ok(mut client) = handle.client.lock() {
if let Err(e) = client.launch(item_id, provider) {
warn!("IPC launch notification failed: {}", e);
}
} else {
warn!("Failed to lock daemon client for launch");
}
}
SearchBackend::Local { frecency, .. } => {
@@ -236,10 +353,16 @@ impl SearchBackend {
#[allow(dead_code)]
pub fn available_provider_ids(&mut self) -> Vec<String> {
match self {
SearchBackend::Daemon(client) => match client.providers() {
Ok(descs) => descs.into_iter().map(|d| d.id).collect(),
SearchBackend::Daemon(handle) => match handle.client.lock() {
Ok(mut client) => match client.providers() {
Ok(descs) => descs.into_iter().map(|d| d.id).collect(),
Err(e) => {
warn!("IPC providers query failed: {}", e);
Vec::new()
}
},
Err(e) => {
warn!("IPC providers query failed: {}", e);
warn!("Failed to lock daemon client: {}", e);
Vec::new()
}
},

View File

@@ -14,6 +14,7 @@
background-color: var(--owlry-bg, @theme_bg_color);
border-radius: var(--owlry-border-radius, 12px);
border: 1px solid var(--owlry-border, @borders);
box-shadow: var(--owlry-shadow, none);
padding: 12px;
}
@@ -56,6 +57,16 @@
color: var(--owlry-accent-bright, @theme_selected_fg_color);
}
/* Highlighted result row (exact match or auto-detected plugin result) */
.owlry-result-highlight {
background-color: alpha(var(--owlry-accent, @theme_selected_bg_color), 0.08);
border-left: 3px solid var(--owlry-accent, @theme_selected_bg_color);
}
.owlry-result-highlight:selected {
border-left: 3px solid var(--owlry-accent-bright, @theme_selected_fg_color);
}
/* Result icon */
.owlry-result-icon {
color: var(--owlry-text, @theme_fg_color);

View File

@@ -31,8 +31,6 @@
.owlry-main {
background-color: rgba(26, 27, 38, 0.95);
border: 1px solid rgba(65, 72, 104, 0.6);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5),
0 0 0 1px rgba(224, 175, 104, 0.1);
}
/* Search entry */

View File

@@ -42,6 +42,8 @@ struct LazyLoadState {
all_results: Vec<LaunchItem>,
/// Number of items currently displayed
displayed_count: usize,
/// The query that produced these results (for highlighting in lazy-loaded batches)
query: String,
}
/// Number of items to display initially and per batch
@@ -224,7 +226,6 @@ impl MainWindow {
main_window.setup_signals();
main_window.setup_lazy_loading();
main_window.update_results("");
// Ensure search entry has focus when window is shown
main_window.search_entry.grab_focus();
@@ -458,7 +459,12 @@ impl MainWindow {
}
/// 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,
lazy_state: &Rc<RefCell<LazyLoadState>>,
) {
let vadj = scrolled.vadjustment();
let row_index = row.index();
@@ -470,15 +476,7 @@ impl MainWindow {
let current_scroll = vadj.value();
let list_height = results_list.height() as f64;
let row_count = {
let mut count = 0;
let mut child = results_list.first_child();
while child.is_some() {
count += 1;
child = child.and_then(|c| c.next_sibling());
}
count.max(1) as f64
};
let row_count = lazy_state.borrow().displayed_count.max(1) as f64;
let row_height = list_height / row_count;
let row_top = row_index as f64 * row_height;
@@ -532,7 +530,7 @@ impl MainWindow {
}
for item in &actions {
let row = ResultRow::new(item);
let row = ResultRow::new(item, "");
results_list.append(&row);
}
@@ -614,7 +612,7 @@ impl MainWindow {
}
for item in &filtered {
let row = ResultRow::new(item);
let row = ResultRow::new(item, "");
results_list.append(&row);
}
@@ -675,6 +673,11 @@ impl MainWindow {
let filter = filter.clone();
let lazy_state = lazy_state.clone();
let debounce_source_for_closure = debounce_source.clone();
let query_str = parsed.query.clone();
let tag = parsed.tag_filter.clone();
// Capture the raw entry text at dispatch time for staleness detection.
let raw_text_at_dispatch = entry.text().to_string();
let search_entry_for_stale = search_entry_for_change.clone();
// Schedule debounced search
let source_id = gtk4::glib::timeout_add_local_once(
@@ -687,40 +690,95 @@ impl MainWindow {
let max_results = cfg.general.max_results;
drop(cfg);
let results = backend.borrow_mut().search_with_tag(
&parsed.query,
max_results,
&filter.borrow(),
&config.borrow(),
parsed.tag_filter.as_deref(),
);
// Try async path (daemon mode)
let receiver = {
let be = backend.borrow();
let f = filter.borrow();
let c = config.borrow();
be.query_async(
&query_str,
max_results,
&f,
&c,
tag.as_deref(),
)
};
// Clear existing results
while let Some(child) = results_list.first_child() {
results_list.remove(&child);
}
if let Some(rx) = receiver {
// Daemon mode: results arrive asynchronously on the main loop.
// spawn_future_local runs the async block on the GTK main
// thread, so non-Send types (Rc, GTK widgets) are fine.
let results_list_cb = results_list.clone();
let current_results_cb = current_results.clone();
let lazy_state_cb = lazy_state.clone();
let query_for_highlight = query_str.clone();
// Lazy loading: store all results but only display initial batch
let initial_count = INITIAL_RESULTS.min(results.len());
{
gtk4::glib::spawn_future_local(async move {
if let Ok(result) = rx.await {
// Discard stale results: the user has typed something new
// since this query was dispatched.
if search_entry_for_stale.text().as_str() != raw_text_at_dispatch {
return;
}
while let Some(child) = results_list_cb.first_child() {
results_list_cb.remove(&child);
}
let items = result.items;
let initial_count =
INITIAL_RESULTS.min(items.len());
for item in items.iter().take(initial_count) {
let row = ResultRow::new(item, &query_for_highlight);
results_list_cb.append(&row);
}
if let Some(first_row) =
results_list_cb.row_at_index(0)
{
results_list_cb.select_row(Some(&first_row));
}
*current_results_cb.borrow_mut() =
items[..initial_count].to_vec();
let mut lazy = lazy_state_cb.borrow_mut();
lazy.all_results = items;
lazy.displayed_count = initial_count;
lazy.query = query_for_highlight;
}
});
} else {
// Local mode (dmenu): synchronous search
let results = backend.borrow_mut().search_with_tag(
&query_str,
max_results,
&filter.borrow(),
&config.borrow(),
tag.as_deref(),
);
while let Some(child) = results_list.first_child() {
results_list.remove(&child);
}
let initial_count = INITIAL_RESULTS.min(results.len());
for item in results.iter().take(initial_count) {
let row = ResultRow::new(item, &query_str);
results_list.append(&row);
}
if let Some(first_row) = results_list.row_at_index(0) {
results_list.select_row(Some(&first_row));
}
*current_results.borrow_mut() =
results[..initial_count].to_vec();
let mut lazy = lazy_state.borrow_mut();
lazy.all_results = results.clone();
lazy.all_results = results;
lazy.query = query_str;
lazy.displayed_count = initial_count;
}
// Display only initial batch
for item in results.iter().take(initial_count) {
let row = ResultRow::new(item);
results_list.append(&row);
}
if let Some(first_row) = results_list.row_at_index(0) {
results_list.select_row(Some(&first_row));
}
// current_results holds only what's displayed (for selection/activation)
*current_results.borrow_mut() =
results.into_iter().take(initial_count).collect();
},
);
@@ -856,6 +914,7 @@ impl MainWindow {
let submenu_state = self.submenu_state.clone();
let tab_order = self.tab_order.clone();
let is_dmenu_mode = self.is_dmenu_mode;
let lazy_state_for_keys = self.lazy_state.clone();
key_controller.connect_key_pressed(move |_, key, _, modifiers| {
let ctrl = modifiers.contains(gtk4::gdk::ModifierType::CONTROL_MASK);
@@ -919,7 +978,7 @@ impl MainWindow {
let next_index = current.index() + 1;
if let Some(next_row) = results_list.row_at_index(next_index) {
results_list.select_row(Some(&next_row));
Self::scroll_to_row(&scrolled, &results_list, &next_row);
Self::scroll_to_row(&scrolled, &results_list, &next_row, &lazy_state_for_keys);
}
}
gtk4::glib::Propagation::Stop
@@ -931,7 +990,7 @@ impl MainWindow {
&& let Some(prev_row) = results_list.row_at_index(prev_index)
{
results_list.select_row(Some(&prev_row));
Self::scroll_to_row(&scrolled, &results_list, &prev_row);
Self::scroll_to_row(&scrolled, &results_list, &prev_row, &lazy_state_for_keys);
}
}
gtk4::glib::Propagation::Stop
@@ -1183,6 +1242,51 @@ impl MainWindow {
entry.emit_by_name::<()>("changed", &[]);
}
/// Schedule initial results population via idle callback.
/// Call this AFTER `window.present()` so the window appears immediately.
pub fn schedule_initial_results(&self) {
let backend = self.backend.clone();
let results_list = self.results_list.clone();
let config = self.config.clone();
let filter = self.filter.clone();
let current_results = self.current_results.clone();
let lazy_state = self.lazy_state.clone();
gtk4::glib::idle_add_local_once(move || {
let cfg = config.borrow();
let max_results = cfg.general.max_results;
drop(cfg);
let results = backend.borrow_mut().search(
"",
max_results,
&filter.borrow(),
&config.borrow(),
);
// Clear existing results
while let Some(child) = results_list.first_child() {
results_list.remove(&child);
}
let initial_count = INITIAL_RESULTS.min(results.len());
for item in results.iter().take(initial_count) {
let row = ResultRow::new(item, "");
results_list.append(&row);
}
if let Some(first_row) = results_list.row_at_index(0) {
results_list.select_row(Some(&first_row));
}
*current_results.borrow_mut() = results[..initial_count].to_vec();
let mut lazy = lazy_state.borrow_mut();
lazy.all_results = results;
lazy.displayed_count = initial_count;
});
}
fn update_results(&self, query: &str) {
let cfg = self.config.borrow();
let max_results = cfg.general.max_results;
@@ -1200,17 +1304,11 @@ impl MainWindow {
self.results_list.remove(&child);
}
// Store all results for lazy loading
let initial_count = INITIAL_RESULTS.min(results.len());
{
let mut lazy = self.lazy_state.borrow_mut();
lazy.all_results = results.clone();
lazy.displayed_count = initial_count;
}
// Display initial batch only
let initial_count = INITIAL_RESULTS.min(results.len());
for item in results.iter().take(initial_count) {
let row = ResultRow::new(item);
let row = ResultRow::new(item, query);
self.results_list.append(&row);
}
@@ -1218,8 +1316,12 @@ impl MainWindow {
self.results_list.select_row(Some(&first_row));
}
// current_results holds what's currently displayed
*self.current_results.borrow_mut() = results.into_iter().take(initial_count).collect();
// current_results holds what's currently displayed; store full vec for lazy loading
*self.current_results.borrow_mut() = results[..initial_count].to_vec();
let mut lazy = self.lazy_state.borrow_mut();
lazy.all_results = results;
lazy.displayed_count = initial_count;
lazy.query = query.to_string();
}
/// Set up lazy loading scroll detection
@@ -1276,8 +1378,9 @@ impl MainWindow {
if displayed < all_count {
// Load next batch
let new_end = (displayed + LOAD_MORE_BATCH).min(all_count);
let query = lazy.query.clone();
for item in lazy.all_results[displayed..new_end].iter() {
let row = ResultRow::new(item);
let row = ResultRow::new(item, &query);
results_list.append(&row);
}
lazy.displayed_count = new_end;

View File

@@ -1,6 +1,6 @@
use gtk4::prelude::*;
use gtk4::{Box as GtkBox, Image, Label, ListBoxRow, Orientation, Widget};
use owlry_core::providers::LaunchItem;
use owlry_core::providers::{LaunchItem, ProviderType};
#[allow(dead_code)]
pub struct ResultRow {
@@ -18,9 +18,31 @@ fn is_emoji_icon(s: &str) -> bool {
!first_char.is_ascii() && s.chars().count() <= 8
}
/// Check if this item should be highlighted based on the query.
/// Highlighted when:
/// - Item is from a dynamic plugin (calculator, converter, websearch, filesearch)
/// and the query is non-empty (auto-detect triggered)
/// - Item name exactly matches the query (case-insensitive)
fn should_highlight(item: &LaunchItem, query: &str) -> bool {
if query.is_empty() {
return false;
}
// Exact name match (case-insensitive)
if item.name.eq_ignore_ascii_case(query) {
return true;
}
// Dynamic plugin auto-detect results
matches!(
&item.provider,
ProviderType::Plugin(id) if matches!(id.as_str(), "calc" | "conv" | "websearch" | "filesearch")
)
}
impl ResultRow {
#[allow(clippy::new_ret_no_self)]
pub fn new(item: &LaunchItem) -> ListBoxRow {
pub fn new(item: &LaunchItem, query: &str) -> ListBoxRow {
let row = ListBoxRow::builder()
.selectable(true)
.activatable(true)
@@ -28,6 +50,10 @@ impl ResultRow {
row.add_css_class("owlry-result-row");
if should_highlight(item, query) {
row.add_css_class("owlry-result-highlight");
}
let hbox = GtkBox::builder()
.orientation(Orientation::Horizontal)
.spacing(12)

View File

@@ -77,8 +77,6 @@
.owlry-main {
background-color: rgba(5, 5, 5, 0.98);
border: 1px solid rgba(38, 38, 38, 0.8);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.8),
0 0 0 1px rgba(255, 0, 68, 0.1);
}
.owlry-search {

View File

@@ -24,8 +24,6 @@
.owlry-main {
background-color: rgba(30, 30, 46, 0.95);
border: 1px solid rgba(69, 71, 90, 0.6);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5),
0 0 0 1px rgba(203, 166, 247, 0.1);
}
.owlry-search {

View File

@@ -24,8 +24,6 @@
.owlry-main {
background-color: rgba(40, 42, 54, 0.95);
border: 1px solid rgba(98, 114, 164, 0.6);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5),
0 0 0 1px rgba(189, 147, 249, 0.1);
}
.owlry-search {

View File

@@ -24,8 +24,6 @@
.owlry-main {
background-color: rgba(40, 40, 40, 0.95);
border: 1px solid rgba(80, 73, 69, 0.6);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5),
0 0 0 1px rgba(254, 128, 25, 0.1);
}
.owlry-search {

View File

@@ -24,8 +24,6 @@
.owlry-main {
background-color: rgba(46, 52, 64, 0.95);
border: 1px solid rgba(76, 86, 106, 0.6);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4),
0 0 0 1px rgba(136, 192, 208, 0.1);
}
.owlry-search {

View File

@@ -24,8 +24,6 @@
.owlry-main {
background-color: rgba(40, 44, 52, 0.95);
border: 1px solid rgba(24, 26, 31, 0.6);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5),
0 0 0 1px rgba(97, 175, 239, 0.1);
}
.owlry-search {

View File

@@ -33,8 +33,6 @@
.owlry-main {
background-color: rgba(26, 27, 38, 0.95);
border: 1px solid rgba(65, 72, 104, 0.6);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5),
0 0 0 1px rgba(224, 175, 104, 0.1);
}
.owlry-search {

View File

@@ -24,8 +24,6 @@
.owlry-main {
background-color: rgba(25, 23, 36, 0.95);
border: 1px solid rgba(38, 35, 58, 0.6);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5),
0 0 0 1px rgba(196, 167, 231, 0.1);
}
.owlry-search {

View File

@@ -24,8 +24,6 @@
.owlry-main {
background-color: rgba(0, 43, 54, 0.95);
border: 1px solid rgba(88, 110, 117, 0.6);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5),
0 0 0 1px rgba(38, 139, 210, 0.1);
}
.owlry-search {

View File

@@ -24,8 +24,6 @@
.owlry-main {
background-color: rgba(26, 27, 38, 0.95);
border: 1px solid rgba(65, 72, 104, 0.6);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5),
0 0 0 1px rgba(122, 162, 247, 0.1);
}
.owlry-search {

566
justfile
View File

@@ -1,65 +1,57 @@
# Owlry build and release automation
# Default recipe
default:
@just --list
# Build debug (all workspace members)
# === Build ===
build:
cargo build --workspace
# Build UI binary only
build-ui:
cargo build -p owlry
# Build core daemon only
build-daemon:
cargo build -p owlry-core
# Build core daemon release
release-daemon:
cargo build -p owlry-core --release
# Run core daemon
run-daemon *ARGS:
cargo run -p owlry-core -- {{ARGS}}
# Build release
release:
cargo build --workspace --release
# Run in debug mode
release-daemon:
cargo build -p owlry-core --release
# === Run ===
run *ARGS:
cargo run -p owlry -- {{ARGS}}
# Run tests
run-daemon *ARGS:
cargo run -p owlry-core -- {{ARGS}}
# === Quality ===
test:
cargo test --workspace
# Check code
check:
cargo check --workspace
cargo clippy --workspace
# Format code
fmt:
cargo fmt --all
# Clean build artifacts
clean:
cargo clean
# Install locally (core + runtimes)
# === Install ===
install-local:
#!/usr/bin/env bash
set -euo pipefail
echo "Building release..."
# Build UI without embedded Lua (smaller binary)
cargo build -p owlry --release --no-default-features
# Build core daemon
cargo build -p owlry-core --release
# Build runtimes
cargo build -p owlry-lua -p owlry-rune --release
echo "Creating directories..."
@@ -71,55 +63,21 @@ install-local:
sudo install -Dm755 target/release/owlry-core /usr/bin/owlry-core
echo "Installing runtimes..."
if [ -f "target/release/libowlry_lua.so" ]; then
sudo install -Dm755 target/release/libowlry_lua.so /usr/lib/owlry/runtimes/liblua.so
echo " → liblua.so"
fi
if [ -f "target/release/libowlry_rune.so" ]; then
sudo install -Dm755 target/release/libowlry_rune.so /usr/lib/owlry/runtimes/librune.so
echo " → librune.so"
fi
[ -f target/release/libowlry_lua.so ] && sudo install -Dm755 target/release/libowlry_lua.so /usr/lib/owlry/runtimes/liblua.so
[ -f target/release/libowlry_rune.so ] && sudo install -Dm755 target/release/libowlry_rune.so /usr/lib/owlry/runtimes/librune.so
echo "Installing systemd service files..."
if [ -f "systemd/owlry-core.service" ]; then
sudo install -Dm644 systemd/owlry-core.service /usr/lib/systemd/user/owlry-core.service
echo " → owlry-core.service"
fi
if [ -f "systemd/owlry-core.socket" ]; then
sudo install -Dm644 systemd/owlry-core.socket /usr/lib/systemd/user/owlry-core.socket
echo " → owlry-core.socket"
fi
[ -f systemd/owlry-core.service ] && sudo install -Dm644 systemd/owlry-core.service /usr/lib/systemd/user/owlry-core.service
[ -f systemd/owlry-core.socket ] && sudo install -Dm644 systemd/owlry-core.socket /usr/lib/systemd/user/owlry-core.socket
echo ""
echo "Installation complete!"
echo " - /usr/bin/owlry (UI)"
echo " - /usr/bin/owlry-core (daemon)"
echo " - $(ls /usr/lib/owlry/runtimes/*.so 2>/dev/null | wc -l) runtimes"
echo " - systemd: owlry-core.service, owlry-core.socket"
echo ""
echo "To start the daemon:"
echo " systemctl --user enable --now owlry-core.service"
echo " OR add 'exec-once = owlry-core' to your compositor config"
echo ""
echo "Note: Install plugins separately from the owlry-plugins repo."
echo "Done. Start daemon: systemctl --user enable --now owlry-core.service"
# === Release Management ===
# === Version Management ===
# AUR package directories (relative to project root)
aur_core_dir := "aur/owlry"
# Get current version from core crate
version := `grep '^version' crates/owlry/Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/'`
# Show current version
show-version:
@echo "Current version: {{version}}"
# Show all crate versions
show-versions:
#!/usr/bin/env bash
echo "=== Crate Versions ==="
for toml in Cargo.toml crates/*/Cargo.toml; do
for toml in crates/*/Cargo.toml; do
name=$(grep '^name' "$toml" | head -1 | sed 's/.*"\(.*\)"/\1/')
ver=$(grep '^version' "$toml" | head -1 | sed 's/.*"\(.*\)"/\1/')
printf " %-30s %s\n" "$name" "$ver"
@@ -129,20 +87,16 @@ show-versions:
crate-version crate:
@grep '^version' crates/{{crate}}/Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/'
# Bump a specific crate version (usage: just bump-crate owlry-core 0.2.0)
# Bump a single crate version, update Cargo.lock, commit
bump-crate crate new_version:
#!/usr/bin/env bash
set -euo pipefail
toml="crates/{{crate}}/Cargo.toml"
if [ ! -f "$toml" ]; then
echo "Error: $toml not found"
exit 1
fi
[ -f "$toml" ] || { echo "Error: $toml not found"; exit 1; }
old=$(grep '^version' "$toml" | head -1 | sed 's/.*"\(.*\)"/\1/')
if [ "$old" = "{{new_version}}" ]; then
echo "{{crate}} is already at {{new_version}}, skipping"
exit 0
fi
[ "$old" = "{{new_version}}" ] && { echo "{{crate}} already at {{new_version}}"; exit 0; }
echo "Bumping {{crate}} from $old to {{new_version}}"
sed -i 's/^version = ".*"/version = "{{new_version}}"/' "$toml"
cargo check -p {{crate}}
@@ -150,7 +104,214 @@ bump-crate crate new_version:
git commit -m "chore({{crate}}): bump version to {{new_version}}"
echo "{{crate}} bumped to {{new_version}}"
# Bump meta-packages (no crate, just AUR version)
# Bump all crates to same version
bump-all new_version:
#!/usr/bin/env bash
set -euo pipefail
for toml in crates/*/Cargo.toml; do
crate=$(basename $(dirname "$toml"))
old=$(grep '^version' "$toml" | head -1 | sed 's/.*"\(.*\)"/\1/')
[ "$old" = "{{new_version}}" ] && continue
echo "Bumping $crate from $old to {{new_version}}"
sed -i 's/^version = ".*"/version = "{{new_version}}"/' "$toml"
done
cargo check --workspace
git add crates/*/Cargo.toml Cargo.lock
git commit -m "chore: bump all crates to {{new_version}}"
echo "All crates bumped to {{new_version}}"
# Bump core UI only
bump new_version:
just bump-crate owlry {{new_version}}
# === Tagging ===
# Tag a specific crate (format: {crate}-v{version})
tag-crate crate:
#!/usr/bin/env bash
set -euo pipefail
ver=$(grep '^version' "crates/{{crate}}/Cargo.toml" | head -1 | sed 's/.*"\(.*\)"/\1/')
tag="{{crate}}-v$ver"
if git rev-parse "$tag" >/dev/null 2>&1; then
echo "Tag $tag already exists"
exit 0
fi
git tag -a "$tag" -m "{{crate}} v$ver"
echo "Created tag $tag"
# Push all local tags
push-tags:
git push --tags
# === AUR Package Management ===
# Stage AUR files into the main repo git index.
# AUR subdirs have their own .git (for aur.archlinux.org), which makes
# git treat them as embedded repos. Temporarily hide .git to stage files.
aur-stage pkg:
#!/usr/bin/env bash
set -euo pipefail
dir="aur/{{pkg}}"
[ -d "$dir" ] || { echo "Error: $dir not found"; exit 1; }
# Build list of files to stage
files=("$dir/PKGBUILD" "$dir/.SRCINFO")
for f in "$dir"/*.install; do
[ -f "$f" ] && files+=("$f")
done
if [ -d "$dir/.git" ]; then
mv "$dir/.git" "$dir/.git.bak"
git add "${files[@]}"
mv "$dir/.git.bak" "$dir/.git"
else
git add "${files[@]}"
fi
# Update a specific AUR package PKGBUILD with correct version + checksum
aur-update-pkg pkg:
#!/usr/bin/env bash
set -euo pipefail
aur_dir="aur/{{pkg}}"
[ -d "$aur_dir" ] || { echo "Error: $aur_dir not found"; exit 1; }
# Determine version
case "{{pkg}}" in
owlry-meta-*)
ver=$(grep '^pkgver=' "$aur_dir/PKGBUILD" | sed 's/pkgver=//')
echo "Meta-package {{pkg}} at $ver (bump pkgrel manually if needed)"
(cd "$aur_dir" && makepkg --printsrcinfo > .SRCINFO)
exit 0
;;
*)
crate_dir="crates/{{pkg}}"
[ -d "$crate_dir" ] || { echo "Error: $crate_dir not found"; exit 1; }
ver=$(grep '^version' "$crate_dir/Cargo.toml" | head -1 | sed 's/.*"\(.*\)"/\1/')
;;
esac
tag="{{pkg}}-v$ver"
url="https://somegit.dev/Owlibou/owlry/archive/$tag.tar.gz"
echo "Updating {{pkg}} to $ver (tag: $tag)"
sed -i "s/^pkgver=.*/pkgver=$ver/" "$aur_dir/PKGBUILD"
sed -i 's/^pkgrel=.*/pkgrel=1/' "$aur_dir/PKGBUILD"
# Update checksum from the tagged tarball
if grep -q "^source=" "$aur_dir/PKGBUILD"; then
echo "Downloading tarball and computing checksum..."
hash=$(curl -sL "$url" | b2sum | cut -d' ' -f1)
if [ -z "$hash" ] || [ ${#hash} -lt 64 ]; then
echo "Error: failed to download or hash $url"
exit 1
fi
sed -i "s|^b2sums=.*|b2sums=('$hash')|" "$aur_dir/PKGBUILD"
fi
(cd "$aur_dir" && makepkg --printsrcinfo > .SRCINFO)
echo "{{pkg}} PKGBUILD updated to $ver"
# Shortcut: update core UI AUR package
aur-update:
just aur-update-pkg owlry
# Publish a specific AUR package to aur.archlinux.org
aur-publish-pkg pkg:
#!/usr/bin/env bash
set -euo pipefail
aur_dir="aur/{{pkg}}"
[ -d "$aur_dir/.git" ] || { echo "Error: $aur_dir has no AUR git repo"; exit 1; }
cd "$aur_dir"
ver=$(grep '^pkgver=' PKGBUILD | sed 's/pkgver=//')
git add PKGBUILD .SRCINFO *.install 2>/dev/null || true
git commit -m "Update to v$ver" || { echo "Nothing to commit"; exit 0; }
git push origin master
echo "{{pkg}} v$ver published to AUR!"
# Shortcut: publish core UI to AUR
aur-publish:
just aur-publish-pkg owlry
# Update and publish ALL AUR packages
aur-update-all:
#!/usr/bin/env bash
set -euo pipefail
for dir in aur/*/; do
pkg=$(basename "$dir")
[ -f "$dir/PKGBUILD" ] || continue
echo "=== $pkg ==="
just aur-update-pkg "$pkg"
echo ""
done
echo "All updated. Run 'just aur-publish-all' to publish."
aur-publish-all:
#!/usr/bin/env bash
set -euo pipefail
for dir in aur/*/; do
pkg=$(basename "$dir")
[ -d "$dir/.git" ] || continue
echo "=== $pkg ==="
just aur-publish-pkg "$pkg"
echo ""
done
echo "All published!"
# Show AUR package status
aur-status:
#!/usr/bin/env bash
echo "=== AUR Package Status ==="
for dir in aur/*/; do
pkg=$(basename "$dir")
[ -f "$dir/PKGBUILD" ] || continue
ver=$(grep '^pkgver=' "$dir/PKGBUILD" | sed 's/pkgver=//')
if [ -d "$dir/.git" ]; then
printf " ✓ %-30s %s\n" "$pkg" "$ver"
else
printf " ✗ %-30s %s (no AUR repo)\n" "$pkg" "$ver"
fi
done
# Commit AUR file changes to the main repo (handles embedded .git dirs)
aur-commit msg="chore(aur): update PKGBUILDs":
#!/usr/bin/env bash
set -euo pipefail
for dir in aur/*/; do
pkg=$(basename "$dir")
[ -f "$dir/PKGBUILD" ] || continue
just aur-stage "$pkg"
done
git diff --cached --quiet && { echo "No AUR changes to commit"; exit 0; }
git commit -m "{{msg}}"
# === Release Workflows ===
# Release a single crate: bump → push → tag → update AUR → publish AUR
release-crate crate new_version:
#!/usr/bin/env bash
set -euo pipefail
just bump-crate {{crate}} {{new_version}}
git push
just tag-crate {{crate}}
just push-tags
echo "Waiting for tag to propagate..."
sleep 3
just aur-update-pkg {{crate}}
just aur-commit "chore(aur): update {{crate}} to {{new_version}}"
git push
just aur-publish-pkg {{crate}}
echo ""
echo "{{crate}} v{{new_version}} released and published to AUR!"
# === Meta Package Management ===
# Bump meta-package versions
bump-meta new_version:
#!/usr/bin/env bash
set -euo pipefail
@@ -165,271 +326,14 @@ bump-meta new_version:
done
echo "Meta-packages bumped to {{new_version}}"
# Bump all crates (core UI + daemon + plugin-api + runtimes) to same version
bump-all new_version:
#!/usr/bin/env bash
set -euo pipefail
for toml in crates/*/Cargo.toml; do
crate=$(basename $(dirname "$toml"))
old=$(grep '^version' "$toml" | head -1 | sed 's/.*"\(.*\)"/\1/')
if [ "$old" != "{{new_version}}" ]; then
echo "Bumping $crate from $old to {{new_version}}"
sed -i 's/^version = ".*"/version = "{{new_version}}"/' "$toml"
fi
done
cargo check --workspace
git add crates/*/Cargo.toml Cargo.lock
git commit -m "chore: bump all crates to {{new_version}}"
echo "All crates bumped to {{new_version}}"
# Bump core version (usage: just bump 0.2.0)
bump new_version:
#!/usr/bin/env bash
set -euo pipefail
if [ "{{version}}" = "{{new_version}}" ]; then
echo "Version is already {{new_version}}, skipping bump"
exit 0
fi
echo "Bumping core version from {{version}} to {{new_version}}"
sed -i 's/^version = ".*"/version = "{{new_version}}"/' crates/owlry/Cargo.toml
cargo check -p owlry
git add crates/owlry/Cargo.toml Cargo.lock
git commit -m "chore: bump version to {{new_version}}"
echo "Version bumped to {{new_version}}"
# Create and push a release tag
tag:
#!/usr/bin/env bash
set -euo pipefail
if git rev-parse "v{{version}}" >/dev/null 2>&1; then
echo "Tag v{{version}} already exists, skipping"
exit 0
fi
echo "Creating tag v{{version}}"
git tag -a "v{{version}}" -m "Release v{{version}}"
git push origin "v{{version}}"
echo "Tag v{{version}} pushed"
# Update AUR package (core UI)
aur-update:
#!/usr/bin/env bash
set -euo pipefail
cd "{{aur_core_dir}}"
url="https://somegit.dev/Owlibou/owlry"
echo "Updating PKGBUILD to version {{version}}"
sed -i 's/^pkgver=.*/pkgver={{version}}/' PKGBUILD
sed -i 's/^pkgrel=.*/pkgrel=1/' PKGBUILD
# Update checksums (b2sums)
echo "Updating checksums..."
b2sum=$(curl -sL "$url/archive/v{{version}}.tar.gz" | b2sum | cut -d' ' -f1)
sed -i "s/^b2sums=.*/b2sums=('$b2sum')/" PKGBUILD
# Generate .SRCINFO
echo "Generating .SRCINFO..."
makepkg --printsrcinfo > .SRCINFO
# Show diff
git diff
echo ""
echo "AUR package updated. Review changes above."
echo "Run 'just aur-publish' to commit and push."
# Publish AUR package (core UI)
aur-publish:
#!/usr/bin/env bash
set -euo pipefail
cd "{{aur_core_dir}}"
git add PKGBUILD .SRCINFO
git commit -m "Update to v{{version}}"
git push
echo "AUR package v{{version}} published!"
# Test AUR package build locally (core UI)
aur-test:
#!/usr/bin/env bash
set -euo pipefail
cd "{{aur_core_dir}}"
echo "Testing PKGBUILD..."
makepkg -sf
echo ""
echo "Package built successfully!"
ls -lh *.pkg.tar.zst
# === AUR Package Management (individual packages) ===
# Update a specific AUR package (usage: just aur-update-pkg owlry-core)
aur-update-pkg pkg:
#!/usr/bin/env bash
set -euo pipefail
aur_dir="aur/{{pkg}}"
if [ ! -d "$aur_dir" ]; then
echo "Error: $aur_dir not found"
exit 1
fi
url="https://somegit.dev/Owlibou/owlry"
# Determine crate version
case "{{pkg}}" in
owlry-meta-essentials|owlry-meta-tools|owlry-meta-widgets|owlry-meta-full)
# Meta-packages use static versioning (1.0.0), only bump pkgrel for dep changes
crate_ver=$(grep '^pkgver=' "$aur_dir/PKGBUILD" | sed 's/pkgver=//')
;;
*)
# Get version from crate
crate_dir="crates/{{pkg}}"
if [ ! -d "$crate_dir" ]; then
echo "Error: $crate_dir not found"
exit 1
fi
crate_ver=$(grep '^version' "$crate_dir/Cargo.toml" | head -1 | sed 's/.*"\(.*\)"/\1/')
;;
esac
cd "$aur_dir"
echo "Updating {{pkg}} PKGBUILD:"
echo " pkgver=$crate_ver"
sed -i "s/^pkgver=.*/pkgver=$crate_ver/" PKGBUILD
sed -i 's/^pkgrel=.*/pkgrel=1/' PKGBUILD
# Update checksums
if grep -q "^source=" PKGBUILD; then
echo "Updating checksums..."
b2sum=$(curl -sL "$url/archive/v$crate_ver.tar.gz" | b2sum | cut -d' ' -f1)
sed -i "s/^b2sums=.*/b2sums=('$b2sum')/" PKGBUILD
fi
# Generate .SRCINFO
echo "Generating .SRCINFO..."
makepkg --printsrcinfo > .SRCINFO
git diff --stat
echo ""
echo "{{pkg}} updated. Run 'just aur-publish-pkg {{pkg}}' to publish."
# Publish a specific AUR package
aur-publish-pkg pkg:
#!/usr/bin/env bash
set -euo pipefail
aur_dir="aur/{{pkg}}"
if [ ! -d "$aur_dir" ]; then
echo "Error: $aur_dir not found"
exit 1
fi
cd "$aur_dir"
ver=$(grep '^pkgver=' PKGBUILD | sed 's/pkgver=//')
git add PKGBUILD .SRCINFO
git commit -m "Update to v$ver"
git push origin master
echo "{{pkg}} v$ver published!"
# === Testing ===
# Test a specific AUR package build locally
aur-test-pkg pkg:
#!/usr/bin/env bash
set -euo pipefail
cd "aur/{{pkg}}"
echo "Testing {{pkg}} PKGBUILD..."
makepkg -sf
echo ""
echo "Package built successfully!"
ls -lh *.pkg.tar.zst
# List all AUR packages with their versions
aur-status:
#!/usr/bin/env bash
echo "=== AUR Package Status ==="
for dir in aur/*/; do
pkg=$(basename "$dir")
if [ -f "$dir/PKGBUILD" ]; then
ver=$(grep '^pkgver=' "$dir/PKGBUILD" | sed 's/pkgver=//')
if [ -d "$dir/.git" ]; then
status="✓"
else
status="✗ (not initialized)"
fi
printf " %s %-30s %s\n" "$status" "$pkg" "$ver"
fi
done
# Update ALL AUR packages (core + daemon + runtimes + meta)
aur-update-all:
#!/usr/bin/env bash
set -euo pipefail
echo "=== Updating core UI ==="
just aur-update
echo ""
echo "=== Updating core daemon ==="
just aur-update-pkg owlry-core
echo ""
echo "=== Updating runtimes ==="
just aur-update-pkg owlry-lua
just aur-update-pkg owlry-rune
echo ""
echo "=== Updating meta-packages ==="
for pkg in owlry-meta-essentials owlry-meta-tools owlry-meta-widgets owlry-meta-full; do
echo "--- $pkg ---"
(cd "aur/$pkg" && makepkg --printsrcinfo > .SRCINFO)
done
echo ""
echo "All AUR packages updated. Run 'just aur-publish-all' to publish."
# Publish ALL AUR packages
aur-publish-all:
#!/usr/bin/env bash
set -euo pipefail
echo "=== Publishing core UI ==="
just aur-publish
echo ""
echo "=== Publishing core daemon ==="
just aur-publish-pkg owlry-core
echo ""
echo "=== Publishing runtimes ==="
just aur-publish-pkg owlry-lua
just aur-publish-pkg owlry-rune
echo ""
echo "=== Publishing meta-packages ==="
for pkg in owlry-meta-essentials owlry-meta-tools owlry-meta-widgets owlry-meta-full; do
echo "--- $pkg ---"
just aur-publish-pkg "$pkg"
done
echo ""
echo "All AUR packages published!"
# Full release workflow for core only (bump + tag + aur)
release-core new_version: (bump new_version)
#!/usr/bin/env bash
set -euo pipefail
# Push version bump
git push
# Create and push tag
just tag
# Wait for tag to be available
echo "Waiting for tag to propagate..."
sleep 2
# Update AUR
just aur-update
echo ""
echo "Core release v{{new_version}} prepared!"
echo "Review AUR changes, then run 'just aur-publish'"