Compare commits
29 Commits
owlry-rune
...
owlry-v1.0
| Author | SHA1 | Date | |
|---|---|---|---|
| c3c35611fd | |||
| 5ecd0a6412 | |||
| 6fe7213b6f | |||
| b768bfd181 | |||
| c9a1ff28f4 | |||
| 623572ec14 | |||
| 5196255594 | |||
| b87447156e | |||
| 12d554959a | |||
| 83fa22d84c | |||
| ade5d3aeef | |||
| 617c943147 | |||
| 1b1e12124b | |||
| 94556f1fe0 | |||
| 2b98f0651c | |||
| 75fa770c94 | |||
| c6ba91f06d | |||
| 235103e854 | |||
| 8ccaaf28c8 | |||
| cfd143fe4a | |||
| 10a685c62f | |||
| 34db33c75f | |||
| 4bff83b5e6 | |||
| 8f7501038d | |||
| 4032205800 | |||
| 99985c7f3b | |||
| 6113217f7b | |||
| 558d415e12 | |||
| 6bde1504b1 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -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
5
Cargo.lock
generated
@@ -2536,12 +2536,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "owlry"
|
||||
version = "1.0.1"
|
||||
version = "1.0.5"
|
||||
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.3"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"ctrlc",
|
||||
|
||||
63
README.md
63
README.md
@@ -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
13
aur/owlry-core/.SRCINFO
Normal file
@@ -0,0 +1,13 @@
|
||||
pkgbase = owlry-core
|
||||
pkgdesc = Core daemon for the Owlry application launcher — manages plugins, providers, and search
|
||||
pkgver = 1.1.2
|
||||
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.2.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-core-v1.1.2.tar.gz
|
||||
b2sums = 2445d097fd6472fd2b6db063ed3f68dc4c5707dfb3fab23b3b9b6b5231732a4df9502112337a513ec7c34a2e613ea895af3bddd54dcfd58b104c0265c1c78034
|
||||
|
||||
pkgname = owlry-core
|
||||
41
aur/owlry-core/PKGBUILD
Normal file
41
aur/owlry-core/PKGBUILD
Normal file
@@ -0,0 +1,41 @@
|
||||
# Maintainer: vikingowl <christian@nachtigall.dev>
|
||||
pkgname=owlry-core
|
||||
pkgver=1.1.2
|
||||
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=('2445d097fd6472fd2b6db063ed3f68dc4c5707dfb3fab23b3b9b6b5231732a4df9502112337a513ec7c34a2e613ea895af3bddd54dcfd58b104c0265c1c78034')
|
||||
|
||||
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
13
aur/owlry-lua/.SRCINFO
Normal 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
40
aur/owlry-lua/PKGBUILD
Normal 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"
|
||||
}
|
||||
19
aur/owlry-meta-essentials/.SRCINFO
Normal file
19
aur/owlry-meta-essentials/.SRCINFO
Normal 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
|
||||
20
aur/owlry-meta-essentials/PKGBUILD
Normal file
20
aur/owlry-meta-essentials/PKGBUILD
Normal 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')
|
||||
29
aur/owlry-meta-full/.SRCINFO
Normal file
29
aur/owlry-meta-full/.SRCINFO
Normal 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
|
||||
35
aur/owlry-meta-full/PKGBUILD
Normal file
35
aur/owlry-meta-full/PKGBUILD
Normal 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')
|
||||
18
aur/owlry-meta-tools/.SRCINFO
Normal file
18
aur/owlry-meta-tools/.SRCINFO
Normal 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
|
||||
19
aur/owlry-meta-tools/PKGBUILD
Normal file
19
aur/owlry-meta-tools/PKGBUILD
Normal 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')
|
||||
16
aur/owlry-meta-widgets/.SRCINFO
Normal file
16
aur/owlry-meta-widgets/.SRCINFO
Normal 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
|
||||
17
aur/owlry-meta-widgets/PKGBUILD
Normal file
17
aur/owlry-meta-widgets/PKGBUILD
Normal 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
13
aur/owlry-rune/.SRCINFO
Normal 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
40
aur/owlry-rune/PKGBUILD
Normal 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
34
aur/owlry/.SRCINFO
Normal file
@@ -0,0 +1,34 @@
|
||||
pkgbase = owlry
|
||||
pkgdesc = Lightweight Wayland application launcher with plugin support
|
||||
pkgver = 1.0.4
|
||||
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.4.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-v1.0.4.tar.gz
|
||||
b2sums = 2445d097fd6472fd2b6db063ed3f68dc4c5707dfb3fab23b3b9b6b5231732a4df9502112337a513ec7c34a2e613ea895af3bddd54dcfd58b104c0265c1c78034
|
||||
|
||||
pkgname = owlry
|
||||
76
aur/owlry/PKGBUILD
Normal file
76
aur/owlry/PKGBUILD
Normal file
@@ -0,0 +1,76 @@
|
||||
# Maintainer: vikingowl <christian@nachtigall.dev>
|
||||
pkgname=owlry
|
||||
pkgver=1.0.4
|
||||
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=('2445d097fd6472fd2b6db063ed3f68dc4c5707dfb3fab23b3b9b6b5231732a4df9502112337a513ec7c34a2e613ea895af3bddd54dcfd58b104c0265c1c78034')
|
||||
|
||||
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/"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "owlry-core"
|
||||
version = "1.1.0"
|
||||
version = "1.1.3"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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:
|
||||
@@ -600,8 +602,20 @@ impl ProviderManager {
|
||||
let dynamic_results = provider.query(query);
|
||||
// Priority comes from plugin-declared priority field
|
||||
let base_score = provider.priority() as i64;
|
||||
|
||||
// Auto-detect plugins (calc, conv) get a grouping bonus so
|
||||
// all their results stay together above generic search results
|
||||
let grouping_bonus: i64 = match provider.provider_type() {
|
||||
ProviderType::Plugin(ref id)
|
||||
if matches!(id.as_str(), "calc" | "conv") =>
|
||||
{
|
||||
10_000
|
||||
}
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
for (idx, item) in dynamic_results.into_iter().enumerate() {
|
||||
results.push((item, base_score - idx as i64));
|
||||
results.push((item, base_score + grouping_bonus - idx as i64));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -633,7 +647,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 +696,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)
|
||||
})
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "owlry"
|
||||
version = "1.0.1"
|
||||
version = "1.0.5"
|
||||
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"
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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,43 +1242,49 @@ impl MainWindow {
|
||||
entry.emit_by_name::<()>("changed", &[]);
|
||||
}
|
||||
|
||||
fn update_results(&self, query: &str) {
|
||||
let cfg = self.config.borrow();
|
||||
let max_results = cfg.general.max_results;
|
||||
drop(cfg);
|
||||
/// 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();
|
||||
|
||||
let results = self.backend.borrow_mut().search(
|
||||
query,
|
||||
max_results,
|
||||
&self.filter.borrow(),
|
||||
&self.config.borrow(),
|
||||
);
|
||||
gtk4::glib::idle_add_local_once(move || {
|
||||
let cfg = config.borrow();
|
||||
let max_results = cfg.general.max_results;
|
||||
drop(cfg);
|
||||
|
||||
// Clear existing results
|
||||
while let Some(child) = self.results_list.first_child() {
|
||||
self.results_list.remove(&child);
|
||||
}
|
||||
let results = backend.borrow_mut().search(
|
||||
"",
|
||||
max_results,
|
||||
&filter.borrow(),
|
||||
&config.borrow(),
|
||||
);
|
||||
|
||||
// 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();
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Display initial batch only
|
||||
for item in results.iter().take(initial_count) {
|
||||
let row = ResultRow::new(item);
|
||||
self.results_list.append(&row);
|
||||
}
|
||||
|
||||
if let Some(first_row) = self.results_list.row_at_index(0) {
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
/// Set up lazy loading scroll detection
|
||||
@@ -1276,8 +1341,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;
|
||||
|
||||
@@ -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 an auto-detecting plugin (calculator, converter) that parsed
|
||||
/// the query into a result — these produce direct answers, not search results
|
||||
/// - 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;
|
||||
}
|
||||
|
||||
// Auto-detect plugins that produce direct answers (not search tools)
|
||||
matches!(
|
||||
&item.provider,
|
||||
ProviderType::Plugin(id) if matches!(id.as_str(), "calc" | "conv")
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
566
justfile
@@ -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 -A
|
||||
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'"
|
||||
|
||||
Reference in New Issue
Block a user