Compare commits
29 Commits
plugin-api
...
owlry-core
| 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
|
/target
|
||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
|
.worktrees/
|
||||||
media.md
|
media.md
|
||||||
|
|
||||||
# AUR packages (each is its own git repo for aur.archlinux.org)
|
# 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/*/.git/
|
||||||
aur/*/pkg/
|
aur/*/pkg/
|
||||||
aur/*/src/
|
aur/*/src/
|
||||||
@@ -10,6 +12,3 @@ aur/*/*.tar.zst
|
|||||||
aur/*/*.tar.gz
|
aur/*/*.tar.gz
|
||||||
aur/*/*.tar.xz
|
aur/*/*.tar.xz
|
||||||
aur/*/*.pkg.tar.*
|
aur/*/*.pkg.tar.*
|
||||||
# Keep PKGBUILD and .SRCINFO tracked
|
|
||||||
.SRCINFO
|
|
||||||
aur/
|
|
||||||
|
|||||||
5
Cargo.lock
generated
5
Cargo.lock
generated
@@ -2536,12 +2536,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "owlry"
|
name = "owlry"
|
||||||
version = "1.0.1"
|
version = "1.0.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"dirs",
|
"dirs",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
"futures-channel",
|
||||||
"glib-build-tools",
|
"glib-build-tools",
|
||||||
"gtk4",
|
"gtk4",
|
||||||
"gtk4-layer-shell",
|
"gtk4-layer-shell",
|
||||||
@@ -2556,7 +2557,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "owlry-core"
|
name = "owlry-core"
|
||||||
version = "1.1.0"
|
version = "1.1.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"ctrlc",
|
"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
|
- **Client/daemon architecture** — Instant window appearance, providers stay loaded in memory
|
||||||
- **Modular plugin architecture** — Install only what you need
|
- **Modular plugin architecture** — Install only what you need
|
||||||
- **Fuzzy search with tags** — Fast matching across names, descriptions, and category tags
|
- **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
|
- **Widget providers** — Weather, media controls, and pomodoro timer at the top of results
|
||||||
- **Config profiles** — Named mode presets for different workflows
|
- **Config profiles** — Named mode presets for different workflows
|
||||||
- **Filter prefixes** — Scope searches with `:app`, `:cmd`, `:tag:development`, etc.
|
- **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
|
yay -S owlry-plugin-calculator owlry-plugin-weather
|
||||||
|
|
||||||
# Or install bundles:
|
# 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-widgets # weather, media, pomodoro
|
||||||
yay -S owlry-meta-tools # clipboard, emoji, websearch, filesearch, systemd
|
yay -S owlry-meta-tools # clipboard, emoji, websearch, filesearch, systemd
|
||||||
yay -S owlry-meta-full # everything
|
yay -S owlry-meta-full # everything
|
||||||
@@ -47,22 +47,42 @@ yay -S owlry-rune # Rune runtime
|
|||||||
|
|
||||||
### Available Packages
|
### 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 |
|
| 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-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-filesearch` | File search (`/ filename`) |
|
||||||
| `owlry-plugin-systemd` | User services with actions |
|
|
||||||
| `owlry-plugin-weather` | Weather widget |
|
|
||||||
| `owlry-plugin-media` | MPRIS media controls |
|
| `owlry-plugin-media` | MPRIS media controls |
|
||||||
| `owlry-plugin-pomodoro` | Pomodoro timer widget |
|
| `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
|
### Build from Source
|
||||||
|
|
||||||
@@ -83,22 +103,29 @@ sudo dnf install gtk4-devel gtk4-layer-shell-devel
|
|||||||
git clone https://somegit.dev/Owlibou/owlry.git
|
git clone https://somegit.dev/Owlibou/owlry.git
|
||||||
cd owlry
|
cd owlry
|
||||||
|
|
||||||
# Build core only (daemon + UI)
|
# Build daemon + UI
|
||||||
cargo build --release -p owlry -p owlry-core
|
cargo build --release -p owlry -p owlry-core
|
||||||
|
|
||||||
# Build specific plugin
|
# Build runtimes (for user plugins)
|
||||||
cargo build --release -p owlry-plugin-calculator
|
cargo build --release -p owlry-lua -p owlry-rune
|
||||||
|
|
||||||
# Build everything
|
# Build everything in this workspace
|
||||||
cargo build --release --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:**
|
**Install locally:**
|
||||||
```bash
|
```bash
|
||||||
just install-local
|
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
|
## 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.
|
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
|
## License
|
||||||
|
|
||||||
GNU General Public License v3.0 — see [LICENSE](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]
|
[package]
|
||||||
name = "owlry-core"
|
name = "owlry-core"
|
||||||
version = "1.1.0"
|
version = "1.1.3"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ use log::{debug, info, warn};
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::Command;
|
|
||||||
|
|
||||||
use crate::paths;
|
use crate::paths;
|
||||||
|
|
||||||
@@ -522,12 +521,15 @@ fn detect_de_terminal() -> Option<String> {
|
|||||||
None
|
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 {
|
fn command_exists(cmd: &str) -> bool {
|
||||||
Command::new("which")
|
std::env::var_os("PATH")
|
||||||
.arg(cmd)
|
.map(|paths| {
|
||||||
.output()
|
std::env::split_paths(&paths).any(|dir| {
|
||||||
.map(|o| o.status.success())
|
let full = dir.join(cmd);
|
||||||
|
full.is_file()
|
||||||
|
})
|
||||||
|
})
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -591,3 +593,17 @@ impl Config {
|
|||||||
Ok(())
|
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
|
/// Calculate frecency using Firefox-style algorithm
|
||||||
fn calculate_frecency(launch_count: u32, last_launch: DateTime<Utc>) -> f64 {
|
fn calculate_frecency(launch_count: u32, last_launch: DateTime<Utc>) -> f64 {
|
||||||
let now = Utc::now();
|
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 = now.signed_duration_since(last_launch);
|
||||||
let age_days = age.num_hours() as f64 / 24.0;
|
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 {
|
let recency_weight = if age_days < 1.0 {
|
||||||
100.0 // Today
|
100.0
|
||||||
} else if age_days < 7.0 {
|
} else if age_days < 7.0 {
|
||||||
70.0 // This week
|
70.0
|
||||||
} else if age_days < 30.0 {
|
} else if age_days < 30.0 {
|
||||||
50.0 // This month
|
50.0
|
||||||
} else if age_days < 90.0 {
|
} else if age_days < 90.0 {
|
||||||
30.0 // This quarter
|
30.0
|
||||||
} else {
|
} else {
|
||||||
10.0 // Older
|
10.0
|
||||||
};
|
};
|
||||||
|
|
||||||
launch_count as f64 * recency_weight
|
launch_count as f64 * recency_weight
|
||||||
@@ -206,6 +219,32 @@ mod tests {
|
|||||||
assert!(score_month < score_week);
|
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]
|
#[test]
|
||||||
fn test_launch_count_matters() {
|
fn test_launch_count_matters() {
|
||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ pub use command::CommandProvider;
|
|||||||
// Re-export native provider for plugin loading
|
// Re-export native provider for plugin loading
|
||||||
pub use native_provider::NativeProvider;
|
pub use native_provider::NativeProvider;
|
||||||
|
|
||||||
|
use chrono::Utc;
|
||||||
use fuzzy_matcher::FuzzyMatcher;
|
use fuzzy_matcher::FuzzyMatcher;
|
||||||
use fuzzy_matcher::skim::SkimMatcherV2;
|
use fuzzy_matcher::skim::SkimMatcherV2;
|
||||||
use log::info;
|
use log::info;
|
||||||
@@ -570,6 +571,7 @@ impl ProviderManager {
|
|||||||
query, max_results, frecency_weight
|
query, max_results, frecency_weight
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let now = Utc::now();
|
||||||
let mut results: Vec<(LaunchItem, i64)> = Vec::new();
|
let mut results: Vec<(LaunchItem, i64)> = Vec::new();
|
||||||
|
|
||||||
// Add widget items first (highest priority) - only when:
|
// Add widget items first (highest priority) - only when:
|
||||||
@@ -600,8 +602,20 @@ impl ProviderManager {
|
|||||||
let dynamic_results = provider.query(query);
|
let dynamic_results = provider.query(query);
|
||||||
// Priority comes from plugin-declared priority field
|
// Priority comes from plugin-declared priority field
|
||||||
let base_score = provider.priority() as i64;
|
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() {
|
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| {
|
.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;
|
let boosted = (frecency_score * frecency_weight * 100.0) as i64;
|
||||||
(item, boosted)
|
(item, boosted)
|
||||||
})
|
})
|
||||||
@@ -682,9 +696,20 @@ impl ProviderManager {
|
|||||||
};
|
};
|
||||||
|
|
||||||
base_score.map(|s| {
|
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;
|
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]
|
[package]
|
||||||
name = "owlry"
|
name = "owlry"
|
||||||
version = "1.0.1"
|
version = "1.0.5"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.90"
|
rust-version = "1.90"
|
||||||
description = "A lightweight, owl-themed application launcher for Wayland"
|
description = "A lightweight, owl-themed application launcher for Wayland"
|
||||||
@@ -46,6 +46,9 @@ dirs = "5"
|
|||||||
# Semantic versioning (needed by plugin commands)
|
# Semantic versioning (needed by plugin commands)
|
||||||
semver = "1"
|
semver = "1"
|
||||||
|
|
||||||
|
# Async oneshot channel (background thread -> main loop)
|
||||||
|
futures-channel = "0.3"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
# GResource compilation for bundled icons
|
# GResource compilation for bundled icons
|
||||||
glib-build-tools = "0.20"
|
glib-build-tools = "0.20"
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ impl OwlryApp {
|
|||||||
match CoreClient::connect_or_start() {
|
match CoreClient::connect_or_start() {
|
||||||
Ok(client) => {
|
Ok(client) => {
|
||||||
info!("Connected to owlry-core daemon");
|
info!("Connected to owlry-core daemon");
|
||||||
SearchBackend::Daemon(client)
|
SearchBackend::Daemon(crate::backend::DaemonHandle::new(client))
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!(
|
warn!(
|
||||||
@@ -135,6 +135,9 @@ impl OwlryApp {
|
|||||||
Self::load_css(&config.borrow());
|
Self::load_css(&config.borrow());
|
||||||
|
|
||||||
window.present();
|
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.
|
/// Create a local backend as fallback when daemon is unavailable.
|
||||||
@@ -182,16 +185,25 @@ impl OwlryApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn setup_icon_theme() {
|
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() {
|
if let Some(display) = gtk4::gdk::Display::default() {
|
||||||
let icon_theme = gtk4::IconTheme::for_display(&display);
|
let icon_theme = gtk4::IconTheme::for_display(&display);
|
||||||
|
|
||||||
// Add Adwaita as fallback search path (has weather and media icons)
|
// If the system icon theme doesn't exist on disk (e.g., set in
|
||||||
icon_theme.add_search_path("/usr/share/icons/Adwaita");
|
// gsettings but not installed), GTK falls back to hicolor which
|
||||||
icon_theme.add_search_path("/usr/share/icons/breeze");
|
// 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::filter::ProviderFilter;
|
||||||
use owlry_core::ipc::ResultItem;
|
use owlry_core::ipc::ResultItem;
|
||||||
use owlry_core::providers::{LaunchItem, ProviderManager, ProviderType};
|
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)
|
/// Backend for search operations. Wraps either an IPC client (daemon mode)
|
||||||
/// or a local ProviderManager (dmenu mode).
|
/// or a local ProviderManager (dmenu mode).
|
||||||
pub enum SearchBackend {
|
pub enum SearchBackend {
|
||||||
/// IPC client connected to owlry-core daemon
|
/// IPC client connected to owlry-core daemon
|
||||||
Daemon(CoreClient),
|
Daemon(DaemonHandle),
|
||||||
/// Direct local provider manager (dmenu mode only)
|
/// Direct local provider manager (dmenu mode only)
|
||||||
Local {
|
Local {
|
||||||
providers: Box<ProviderManager>,
|
providers: Box<ProviderManager>,
|
||||||
@@ -24,6 +99,22 @@ pub enum SearchBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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.
|
/// Search for items matching the query.
|
||||||
///
|
///
|
||||||
/// In daemon mode, sends query over IPC. The modes list is derived from
|
/// In daemon mode, sends query over IPC. The modes list is derived from
|
||||||
@@ -38,24 +129,18 @@ impl SearchBackend {
|
|||||||
config: &Config,
|
config: &Config,
|
||||||
) -> Vec<LaunchItem> {
|
) -> Vec<LaunchItem> {
|
||||||
match self {
|
match self {
|
||||||
SearchBackend::Daemon(client) => {
|
SearchBackend::Daemon(handle) => {
|
||||||
// When accept_all, send None so daemon doesn't restrict to a specific set
|
let modes_param = Self::build_modes_param(filter);
|
||||||
// (otherwise dynamically loaded plugin types would be filtered out)
|
match handle.client.lock() {
|
||||||
let modes_param = if filter.is_accept_all() {
|
Ok(mut client) => match client.query(query, modes_param) {
|
||||||
None
|
Ok(items) => items.into_iter().map(result_to_launch_item).collect(),
|
||||||
} else {
|
Err(e) => {
|
||||||
let modes: Vec<String> = filter
|
warn!("IPC query failed: {}", e);
|
||||||
.enabled_providers()
|
Vec::new()
|
||||||
.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(),
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("IPC query failed: {}", e);
|
warn!("Failed to lock daemon client: {}", e);
|
||||||
Vec::new()
|
Vec::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -101,32 +186,24 @@ impl SearchBackend {
|
|||||||
tag_filter: Option<&str>,
|
tag_filter: Option<&str>,
|
||||||
) -> Vec<LaunchItem> {
|
) -> Vec<LaunchItem> {
|
||||||
match self {
|
match self {
|
||||||
SearchBackend::Daemon(client) => {
|
SearchBackend::Daemon(handle) => {
|
||||||
// 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.
|
|
||||||
let effective_query = if let Some(tag) = tag_filter {
|
let effective_query = if let Some(tag) = tag_filter {
|
||||||
format!(":tag:{} {}", tag, query)
|
format!(":tag:{} {}", tag, query)
|
||||||
} else {
|
} else {
|
||||||
query.to_string()
|
query.to_string()
|
||||||
};
|
};
|
||||||
|
|
||||||
// When accept_all, send None so daemon doesn't restrict to a specific set
|
let modes_param = Self::build_modes_param(filter);
|
||||||
// (otherwise dynamically loaded plugin types would be filtered out)
|
match handle.client.lock() {
|
||||||
let modes_param = if filter.is_accept_all() {
|
Ok(mut client) => match client.query(&effective_query, modes_param) {
|
||||||
None
|
Ok(items) => items.into_iter().map(result_to_launch_item).collect(),
|
||||||
} else {
|
Err(e) => {
|
||||||
let modes: Vec<String> = filter
|
warn!("IPC query failed: {}", e);
|
||||||
.enabled_providers()
|
Vec::new()
|
||||||
.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(),
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("IPC query failed: {}", e);
|
warn!("Failed to lock daemon client: {}", e);
|
||||||
Vec::new()
|
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.
|
/// Execute a plugin action command. Returns true if handled.
|
||||||
pub fn execute_plugin_action(&mut self, command: &str) -> bool {
|
pub fn execute_plugin_action(&mut self, command: &str) -> bool {
|
||||||
match self {
|
match self {
|
||||||
SearchBackend::Daemon(client) => match client.plugin_action(command) {
|
SearchBackend::Daemon(handle) => match handle.client.lock() {
|
||||||
Ok(handled) => handled,
|
Ok(mut client) => match client.plugin_action(command) {
|
||||||
|
Ok(handled) => handled,
|
||||||
|
Err(e) => {
|
||||||
|
warn!("IPC plugin_action failed: {}", e);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("IPC plugin_action failed: {}", e);
|
warn!("Failed to lock daemon client: {}", e);
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -185,15 +292,21 @@ impl SearchBackend {
|
|||||||
display_name: &str,
|
display_name: &str,
|
||||||
) -> Option<(String, Vec<LaunchItem>)> {
|
) -> Option<(String, Vec<LaunchItem>)> {
|
||||||
match self {
|
match self {
|
||||||
SearchBackend::Daemon(client) => match client.submenu(plugin_id, data) {
|
SearchBackend::Daemon(handle) => match handle.client.lock() {
|
||||||
Ok(items) if !items.is_empty() => {
|
Ok(mut client) => match client.submenu(plugin_id, data) {
|
||||||
let actions: Vec<LaunchItem> =
|
Ok(items) if !items.is_empty() => {
|
||||||
items.into_iter().map(result_to_launch_item).collect();
|
let actions: Vec<LaunchItem> =
|
||||||
Some((display_name.to_string(), actions))
|
items.into_iter().map(result_to_launch_item).collect();
|
||||||
}
|
Some((display_name.to_string(), actions))
|
||||||
Ok(_) => None,
|
}
|
||||||
|
Ok(_) => None,
|
||||||
|
Err(e) => {
|
||||||
|
warn!("IPC submenu query failed: {}", e);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("IPC submenu query failed: {}", e);
|
warn!("Failed to lock daemon client: {}", e);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -206,9 +319,13 @@ impl SearchBackend {
|
|||||||
/// Record a launch event for frecency tracking.
|
/// Record a launch event for frecency tracking.
|
||||||
pub fn record_launch(&mut self, item_id: &str, provider: &str) {
|
pub fn record_launch(&mut self, item_id: &str, provider: &str) {
|
||||||
match self {
|
match self {
|
||||||
SearchBackend::Daemon(client) => {
|
SearchBackend::Daemon(handle) => {
|
||||||
if let Err(e) = client.launch(item_id, provider) {
|
if let Ok(mut client) = handle.client.lock() {
|
||||||
warn!("IPC launch notification failed: {}", e);
|
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, .. } => {
|
SearchBackend::Local { frecency, .. } => {
|
||||||
@@ -236,10 +353,16 @@ impl SearchBackend {
|
|||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn available_provider_ids(&mut self) -> Vec<String> {
|
pub fn available_provider_ids(&mut self) -> Vec<String> {
|
||||||
match self {
|
match self {
|
||||||
SearchBackend::Daemon(client) => match client.providers() {
|
SearchBackend::Daemon(handle) => match handle.client.lock() {
|
||||||
Ok(descs) => descs.into_iter().map(|d| d.id).collect(),
|
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) => {
|
Err(e) => {
|
||||||
warn!("IPC providers query failed: {}", e);
|
warn!("Failed to lock daemon client: {}", e);
|
||||||
Vec::new()
|
Vec::new()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
background-color: var(--owlry-bg, @theme_bg_color);
|
background-color: var(--owlry-bg, @theme_bg_color);
|
||||||
border-radius: var(--owlry-border-radius, 12px);
|
border-radius: var(--owlry-border-radius, 12px);
|
||||||
border: 1px solid var(--owlry-border, @borders);
|
border: 1px solid var(--owlry-border, @borders);
|
||||||
|
box-shadow: var(--owlry-shadow, none);
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,6 +57,16 @@
|
|||||||
color: var(--owlry-accent-bright, @theme_selected_fg_color);
|
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 */
|
/* Result icon */
|
||||||
.owlry-result-icon {
|
.owlry-result-icon {
|
||||||
color: var(--owlry-text, @theme_fg_color);
|
color: var(--owlry-text, @theme_fg_color);
|
||||||
|
|||||||
@@ -31,8 +31,6 @@
|
|||||||
.owlry-main {
|
.owlry-main {
|
||||||
background-color: rgba(26, 27, 38, 0.95);
|
background-color: rgba(26, 27, 38, 0.95);
|
||||||
border: 1px solid rgba(65, 72, 104, 0.6);
|
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 */
|
/* Search entry */
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ struct LazyLoadState {
|
|||||||
all_results: Vec<LaunchItem>,
|
all_results: Vec<LaunchItem>,
|
||||||
/// Number of items currently displayed
|
/// Number of items currently displayed
|
||||||
displayed_count: usize,
|
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
|
/// Number of items to display initially and per batch
|
||||||
@@ -224,7 +226,6 @@ impl MainWindow {
|
|||||||
|
|
||||||
main_window.setup_signals();
|
main_window.setup_signals();
|
||||||
main_window.setup_lazy_loading();
|
main_window.setup_lazy_loading();
|
||||||
main_window.update_results("");
|
|
||||||
|
|
||||||
// Ensure search entry has focus when window is shown
|
// Ensure search entry has focus when window is shown
|
||||||
main_window.search_entry.grab_focus();
|
main_window.search_entry.grab_focus();
|
||||||
@@ -458,7 +459,12 @@ impl MainWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Scroll the given row into view within the scrolled window
|
/// Scroll the given row into view within the scrolled window
|
||||||
fn scroll_to_row(scrolled: &ScrolledWindow, results_list: &ListBox, row: &ListBoxRow) {
|
fn scroll_to_row(
|
||||||
|
scrolled: &ScrolledWindow,
|
||||||
|
results_list: &ListBox,
|
||||||
|
row: &ListBoxRow,
|
||||||
|
lazy_state: &Rc<RefCell<LazyLoadState>>,
|
||||||
|
) {
|
||||||
let vadj = scrolled.vadjustment();
|
let vadj = scrolled.vadjustment();
|
||||||
|
|
||||||
let row_index = row.index();
|
let row_index = row.index();
|
||||||
@@ -470,15 +476,7 @@ impl MainWindow {
|
|||||||
let current_scroll = vadj.value();
|
let current_scroll = vadj.value();
|
||||||
|
|
||||||
let list_height = results_list.height() as f64;
|
let list_height = results_list.height() as f64;
|
||||||
let row_count = {
|
let row_count = lazy_state.borrow().displayed_count.max(1) as f64;
|
||||||
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_height = list_height / row_count;
|
let row_height = list_height / row_count;
|
||||||
let row_top = row_index as f64 * row_height;
|
let row_top = row_index as f64 * row_height;
|
||||||
@@ -532,7 +530,7 @@ impl MainWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for item in &actions {
|
for item in &actions {
|
||||||
let row = ResultRow::new(item);
|
let row = ResultRow::new(item, "");
|
||||||
results_list.append(&row);
|
results_list.append(&row);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -614,7 +612,7 @@ impl MainWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for item in &filtered {
|
for item in &filtered {
|
||||||
let row = ResultRow::new(item);
|
let row = ResultRow::new(item, "");
|
||||||
results_list.append(&row);
|
results_list.append(&row);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -675,6 +673,11 @@ impl MainWindow {
|
|||||||
let filter = filter.clone();
|
let filter = filter.clone();
|
||||||
let lazy_state = lazy_state.clone();
|
let lazy_state = lazy_state.clone();
|
||||||
let debounce_source_for_closure = debounce_source.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
|
// Schedule debounced search
|
||||||
let source_id = gtk4::glib::timeout_add_local_once(
|
let source_id = gtk4::glib::timeout_add_local_once(
|
||||||
@@ -687,40 +690,95 @@ impl MainWindow {
|
|||||||
let max_results = cfg.general.max_results;
|
let max_results = cfg.general.max_results;
|
||||||
drop(cfg);
|
drop(cfg);
|
||||||
|
|
||||||
let results = backend.borrow_mut().search_with_tag(
|
// Try async path (daemon mode)
|
||||||
&parsed.query,
|
let receiver = {
|
||||||
max_results,
|
let be = backend.borrow();
|
||||||
&filter.borrow(),
|
let f = filter.borrow();
|
||||||
&config.borrow(),
|
let c = config.borrow();
|
||||||
parsed.tag_filter.as_deref(),
|
be.query_async(
|
||||||
);
|
&query_str,
|
||||||
|
max_results,
|
||||||
|
&f,
|
||||||
|
&c,
|
||||||
|
tag.as_deref(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
// Clear existing results
|
if let Some(rx) = receiver {
|
||||||
while let Some(child) = results_list.first_child() {
|
// Daemon mode: results arrive asynchronously on the main loop.
|
||||||
results_list.remove(&child);
|
// 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
|
gtk4::glib::spawn_future_local(async move {
|
||||||
let initial_count = INITIAL_RESULTS.min(results.len());
|
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();
|
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;
|
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 submenu_state = self.submenu_state.clone();
|
||||||
let tab_order = self.tab_order.clone();
|
let tab_order = self.tab_order.clone();
|
||||||
let is_dmenu_mode = self.is_dmenu_mode;
|
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| {
|
key_controller.connect_key_pressed(move |_, key, _, modifiers| {
|
||||||
let ctrl = modifiers.contains(gtk4::gdk::ModifierType::CONTROL_MASK);
|
let ctrl = modifiers.contains(gtk4::gdk::ModifierType::CONTROL_MASK);
|
||||||
@@ -919,7 +978,7 @@ impl MainWindow {
|
|||||||
let next_index = current.index() + 1;
|
let next_index = current.index() + 1;
|
||||||
if let Some(next_row) = results_list.row_at_index(next_index) {
|
if let Some(next_row) = results_list.row_at_index(next_index) {
|
||||||
results_list.select_row(Some(&next_row));
|
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
|
gtk4::glib::Propagation::Stop
|
||||||
@@ -931,7 +990,7 @@ impl MainWindow {
|
|||||||
&& let Some(prev_row) = results_list.row_at_index(prev_index)
|
&& let Some(prev_row) = results_list.row_at_index(prev_index)
|
||||||
{
|
{
|
||||||
results_list.select_row(Some(&prev_row));
|
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
|
gtk4::glib::Propagation::Stop
|
||||||
@@ -1183,43 +1242,49 @@ impl MainWindow {
|
|||||||
entry.emit_by_name::<()>("changed", &[]);
|
entry.emit_by_name::<()>("changed", &[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_results(&self, query: &str) {
|
/// Schedule initial results population via idle callback.
|
||||||
let cfg = self.config.borrow();
|
/// Call this AFTER `window.present()` so the window appears immediately.
|
||||||
let max_results = cfg.general.max_results;
|
pub fn schedule_initial_results(&self) {
|
||||||
drop(cfg);
|
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(
|
gtk4::glib::idle_add_local_once(move || {
|
||||||
query,
|
let cfg = config.borrow();
|
||||||
max_results,
|
let max_results = cfg.general.max_results;
|
||||||
&self.filter.borrow(),
|
drop(cfg);
|
||||||
&self.config.borrow(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Clear existing results
|
let results = backend.borrow_mut().search(
|
||||||
while let Some(child) = self.results_list.first_child() {
|
"",
|
||||||
self.results_list.remove(&child);
|
max_results,
|
||||||
}
|
&filter.borrow(),
|
||||||
|
&config.borrow(),
|
||||||
|
);
|
||||||
|
|
||||||
// Store all results for lazy loading
|
// Clear existing results
|
||||||
let initial_count = INITIAL_RESULTS.min(results.len());
|
while let Some(child) = results_list.first_child() {
|
||||||
{
|
results_list.remove(&child);
|
||||||
let mut lazy = self.lazy_state.borrow_mut();
|
}
|
||||||
lazy.all_results = results.clone();
|
|
||||||
|
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;
|
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
|
/// Set up lazy loading scroll detection
|
||||||
@@ -1276,8 +1341,9 @@ impl MainWindow {
|
|||||||
if displayed < all_count {
|
if displayed < all_count {
|
||||||
// Load next batch
|
// Load next batch
|
||||||
let new_end = (displayed + LOAD_MORE_BATCH).min(all_count);
|
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() {
|
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);
|
results_list.append(&row);
|
||||||
}
|
}
|
||||||
lazy.displayed_count = new_end;
|
lazy.displayed_count = new_end;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use gtk4::prelude::*;
|
use gtk4::prelude::*;
|
||||||
use gtk4::{Box as GtkBox, Image, Label, ListBoxRow, Orientation, Widget};
|
use gtk4::{Box as GtkBox, Image, Label, ListBoxRow, Orientation, Widget};
|
||||||
use owlry_core::providers::LaunchItem;
|
use owlry_core::providers::{LaunchItem, ProviderType};
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub struct ResultRow {
|
pub struct ResultRow {
|
||||||
@@ -18,9 +18,31 @@ fn is_emoji_icon(s: &str) -> bool {
|
|||||||
!first_char.is_ascii() && s.chars().count() <= 8
|
!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 {
|
impl ResultRow {
|
||||||
#[allow(clippy::new_ret_no_self)]
|
#[allow(clippy::new_ret_no_self)]
|
||||||
pub fn new(item: &LaunchItem) -> ListBoxRow {
|
pub fn new(item: &LaunchItem, query: &str) -> ListBoxRow {
|
||||||
let row = ListBoxRow::builder()
|
let row = ListBoxRow::builder()
|
||||||
.selectable(true)
|
.selectable(true)
|
||||||
.activatable(true)
|
.activatable(true)
|
||||||
@@ -28,6 +50,10 @@ impl ResultRow {
|
|||||||
|
|
||||||
row.add_css_class("owlry-result-row");
|
row.add_css_class("owlry-result-row");
|
||||||
|
|
||||||
|
if should_highlight(item, query) {
|
||||||
|
row.add_css_class("owlry-result-highlight");
|
||||||
|
}
|
||||||
|
|
||||||
let hbox = GtkBox::builder()
|
let hbox = GtkBox::builder()
|
||||||
.orientation(Orientation::Horizontal)
|
.orientation(Orientation::Horizontal)
|
||||||
.spacing(12)
|
.spacing(12)
|
||||||
|
|||||||
@@ -77,8 +77,6 @@
|
|||||||
.owlry-main {
|
.owlry-main {
|
||||||
background-color: rgba(5, 5, 5, 0.98);
|
background-color: rgba(5, 5, 5, 0.98);
|
||||||
border: 1px solid rgba(38, 38, 38, 0.8);
|
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 {
|
.owlry-search {
|
||||||
|
|||||||
@@ -24,8 +24,6 @@
|
|||||||
.owlry-main {
|
.owlry-main {
|
||||||
background-color: rgba(30, 30, 46, 0.95);
|
background-color: rgba(30, 30, 46, 0.95);
|
||||||
border: 1px solid rgba(69, 71, 90, 0.6);
|
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 {
|
.owlry-search {
|
||||||
|
|||||||
@@ -24,8 +24,6 @@
|
|||||||
.owlry-main {
|
.owlry-main {
|
||||||
background-color: rgba(40, 42, 54, 0.95);
|
background-color: rgba(40, 42, 54, 0.95);
|
||||||
border: 1px solid rgba(98, 114, 164, 0.6);
|
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 {
|
.owlry-search {
|
||||||
|
|||||||
@@ -24,8 +24,6 @@
|
|||||||
.owlry-main {
|
.owlry-main {
|
||||||
background-color: rgba(40, 40, 40, 0.95);
|
background-color: rgba(40, 40, 40, 0.95);
|
||||||
border: 1px solid rgba(80, 73, 69, 0.6);
|
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 {
|
.owlry-search {
|
||||||
|
|||||||
@@ -24,8 +24,6 @@
|
|||||||
.owlry-main {
|
.owlry-main {
|
||||||
background-color: rgba(46, 52, 64, 0.95);
|
background-color: rgba(46, 52, 64, 0.95);
|
||||||
border: 1px solid rgba(76, 86, 106, 0.6);
|
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 {
|
.owlry-search {
|
||||||
|
|||||||
@@ -24,8 +24,6 @@
|
|||||||
.owlry-main {
|
.owlry-main {
|
||||||
background-color: rgba(40, 44, 52, 0.95);
|
background-color: rgba(40, 44, 52, 0.95);
|
||||||
border: 1px solid rgba(24, 26, 31, 0.6);
|
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 {
|
.owlry-search {
|
||||||
|
|||||||
@@ -33,8 +33,6 @@
|
|||||||
.owlry-main {
|
.owlry-main {
|
||||||
background-color: rgba(26, 27, 38, 0.95);
|
background-color: rgba(26, 27, 38, 0.95);
|
||||||
border: 1px solid rgba(65, 72, 104, 0.6);
|
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 {
|
.owlry-search {
|
||||||
|
|||||||
@@ -24,8 +24,6 @@
|
|||||||
.owlry-main {
|
.owlry-main {
|
||||||
background-color: rgba(25, 23, 36, 0.95);
|
background-color: rgba(25, 23, 36, 0.95);
|
||||||
border: 1px solid rgba(38, 35, 58, 0.6);
|
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 {
|
.owlry-search {
|
||||||
|
|||||||
@@ -24,8 +24,6 @@
|
|||||||
.owlry-main {
|
.owlry-main {
|
||||||
background-color: rgba(0, 43, 54, 0.95);
|
background-color: rgba(0, 43, 54, 0.95);
|
||||||
border: 1px solid rgba(88, 110, 117, 0.6);
|
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 {
|
.owlry-search {
|
||||||
|
|||||||
@@ -24,8 +24,6 @@
|
|||||||
.owlry-main {
|
.owlry-main {
|
||||||
background-color: rgba(26, 27, 38, 0.95);
|
background-color: rgba(26, 27, 38, 0.95);
|
||||||
border: 1px solid rgba(65, 72, 104, 0.6);
|
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 {
|
.owlry-search {
|
||||||
|
|||||||
566
justfile
566
justfile
@@ -1,65 +1,57 @@
|
|||||||
# Owlry build and release automation
|
# Owlry build and release automation
|
||||||
|
|
||||||
# Default recipe
|
|
||||||
default:
|
default:
|
||||||
@just --list
|
@just --list
|
||||||
|
|
||||||
# Build debug (all workspace members)
|
# === Build ===
|
||||||
|
|
||||||
build:
|
build:
|
||||||
cargo build --workspace
|
cargo build --workspace
|
||||||
|
|
||||||
# Build UI binary only
|
|
||||||
build-ui:
|
build-ui:
|
||||||
cargo build -p owlry
|
cargo build -p owlry
|
||||||
|
|
||||||
# Build core daemon only
|
|
||||||
build-daemon:
|
build-daemon:
|
||||||
cargo build -p owlry-core
|
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:
|
release:
|
||||||
cargo build --workspace --release
|
cargo build --workspace --release
|
||||||
|
|
||||||
# Run in debug mode
|
release-daemon:
|
||||||
|
cargo build -p owlry-core --release
|
||||||
|
|
||||||
|
# === Run ===
|
||||||
|
|
||||||
run *ARGS:
|
run *ARGS:
|
||||||
cargo run -p owlry -- {{ARGS}}
|
cargo run -p owlry -- {{ARGS}}
|
||||||
|
|
||||||
# Run tests
|
run-daemon *ARGS:
|
||||||
|
cargo run -p owlry-core -- {{ARGS}}
|
||||||
|
|
||||||
|
# === Quality ===
|
||||||
|
|
||||||
test:
|
test:
|
||||||
cargo test --workspace
|
cargo test --workspace
|
||||||
|
|
||||||
# Check code
|
|
||||||
check:
|
check:
|
||||||
cargo check --workspace
|
cargo check --workspace
|
||||||
cargo clippy --workspace
|
cargo clippy --workspace
|
||||||
|
|
||||||
# Format code
|
|
||||||
fmt:
|
fmt:
|
||||||
cargo fmt --all
|
cargo fmt --all
|
||||||
|
|
||||||
# Clean build artifacts
|
|
||||||
clean:
|
clean:
|
||||||
cargo clean
|
cargo clean
|
||||||
|
|
||||||
# Install locally (core + runtimes)
|
# === Install ===
|
||||||
|
|
||||||
install-local:
|
install-local:
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
echo "Building release..."
|
echo "Building release..."
|
||||||
# Build UI without embedded Lua (smaller binary)
|
|
||||||
cargo build -p owlry --release --no-default-features
|
cargo build -p owlry --release --no-default-features
|
||||||
# Build core daemon
|
|
||||||
cargo build -p owlry-core --release
|
cargo build -p owlry-core --release
|
||||||
# Build runtimes
|
|
||||||
cargo build -p owlry-lua -p owlry-rune --release
|
cargo build -p owlry-lua -p owlry-rune --release
|
||||||
|
|
||||||
echo "Creating directories..."
|
echo "Creating directories..."
|
||||||
@@ -71,55 +63,21 @@ install-local:
|
|||||||
sudo install -Dm755 target/release/owlry-core /usr/bin/owlry-core
|
sudo install -Dm755 target/release/owlry-core /usr/bin/owlry-core
|
||||||
|
|
||||||
echo "Installing runtimes..."
|
echo "Installing runtimes..."
|
||||||
if [ -f "target/release/libowlry_lua.so" ]; then
|
[ -f target/release/libowlry_lua.so ] && sudo install -Dm755 target/release/libowlry_lua.so /usr/lib/owlry/runtimes/liblua.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 " → 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
|
|
||||||
|
|
||||||
echo "Installing systemd service files..."
|
echo "Installing systemd service files..."
|
||||||
if [ -f "systemd/owlry-core.service" ]; then
|
[ -f systemd/owlry-core.service ] && sudo install -Dm644 systemd/owlry-core.service /usr/lib/systemd/user/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 " → 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
|
|
||||||
|
|
||||||
echo ""
|
echo "Done. Start daemon: systemctl --user enable --now owlry-core.service"
|
||||||
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."
|
|
||||||
|
|
||||||
# === 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:
|
show-versions:
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
echo "=== Crate Versions ==="
|
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/')
|
name=$(grep '^name' "$toml" | head -1 | sed 's/.*"\(.*\)"/\1/')
|
||||||
ver=$(grep '^version' "$toml" | head -1 | sed 's/.*"\(.*\)"/\1/')
|
ver=$(grep '^version' "$toml" | head -1 | sed 's/.*"\(.*\)"/\1/')
|
||||||
printf " %-30s %s\n" "$name" "$ver"
|
printf " %-30s %s\n" "$name" "$ver"
|
||||||
@@ -129,20 +87,16 @@ show-versions:
|
|||||||
crate-version crate:
|
crate-version crate:
|
||||||
@grep '^version' crates/{{crate}}/Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/'
|
@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:
|
bump-crate crate new_version:
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
toml="crates/{{crate}}/Cargo.toml"
|
toml="crates/{{crate}}/Cargo.toml"
|
||||||
if [ ! -f "$toml" ]; then
|
[ -f "$toml" ] || { echo "Error: $toml not found"; exit 1; }
|
||||||
echo "Error: $toml not found"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
old=$(grep '^version' "$toml" | head -1 | sed 's/.*"\(.*\)"/\1/')
|
old=$(grep '^version' "$toml" | head -1 | sed 's/.*"\(.*\)"/\1/')
|
||||||
if [ "$old" = "{{new_version}}" ]; then
|
[ "$old" = "{{new_version}}" ] && { echo "{{crate}} already at {{new_version}}"; exit 0; }
|
||||||
echo "{{crate}} is already at {{new_version}}, skipping"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
echo "Bumping {{crate}} from $old to {{new_version}}"
|
echo "Bumping {{crate}} from $old to {{new_version}}"
|
||||||
sed -i 's/^version = ".*"/version = "{{new_version}}"/' "$toml"
|
sed -i 's/^version = ".*"/version = "{{new_version}}"/' "$toml"
|
||||||
cargo check -p {{crate}}
|
cargo check -p {{crate}}
|
||||||
@@ -150,7 +104,214 @@ bump-crate crate new_version:
|
|||||||
git commit -m "chore({{crate}}): bump version to {{new_version}}"
|
git commit -m "chore({{crate}}): bump version to {{new_version}}"
|
||||||
echo "{{crate}} bumped 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:
|
bump-meta new_version:
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@@ -165,271 +326,14 @@ bump-meta new_version:
|
|||||||
done
|
done
|
||||||
echo "Meta-packages bumped to {{new_version}}"
|
echo "Meta-packages bumped to {{new_version}}"
|
||||||
|
|
||||||
# Bump all crates (core UI + daemon + plugin-api + runtimes) to same version
|
# === Testing ===
|
||||||
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!"
|
|
||||||
|
|
||||||
# Test a specific AUR package build locally
|
# Test a specific AUR package build locally
|
||||||
aur-test-pkg pkg:
|
aur-test-pkg pkg:
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
cd "aur/{{pkg}}"
|
cd "aur/{{pkg}}"
|
||||||
|
|
||||||
echo "Testing {{pkg}} PKGBUILD..."
|
echo "Testing {{pkg}} PKGBUILD..."
|
||||||
makepkg -sf
|
makepkg -sf
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "Package built successfully!"
|
echo "Package built successfully!"
|
||||||
ls -lh *.pkg.tar.zst
|
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