Extract bos-settings into its own repo; add Appearance/Startup Apps/Display editors
Split out of the bos repo so a bos-settings release no longer requires a BOS ISO release (and vice versa) — CI here publishes straight to the [breadway] pacman repo on tag push, independent of bos's own release cadence. Also adds real GUI editors for the Hyprland JSON config surfaces bos gained recently (hypr/settings.json, hypr/monitors.json, hypr/autostart.json): - Appearance panel: gaps, borders (with a real color picker), blur, shadow, tiling layout, and basic input settings. - Display panel: a monitor-rule list editor alongside the existing live connected-monitor readout. - Startup Apps panel: toggle/edit/add/remove the extra autostart commands. - --page argv support for deep-linking (breadhelp's guides can now open bos-settings straight to a specific page). - Fixed a stale "View keybinds cheat sheet" button pointing at a file deleted when breadhelp replaced bos-keybinds/bos-welcome.
This commit is contained in:
parent
d0fc16bc84
commit
121e93d273
14 changed files with 1801 additions and 30 deletions
21
.forgejo/workflows/mirror.yml
Normal file
21
.forgejo/workflows/mirror.yml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
name: Mirror to GitHub
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['**']
|
||||
tags: ['**']
|
||||
|
||||
jobs:
|
||||
mirror:
|
||||
runs-on: [self-hosted, hestia]
|
||||
steps:
|
||||
- name: Mirror to GitHub
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git clone --mirror "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" repo.git
|
||||
cd repo.git
|
||||
# Mirror only branches and tags (not refs/pull/*, which GitHub rejects);
|
||||
# --prune deletes GitHub refs that no longer exist on Forgejo.
|
||||
git push --prune \
|
||||
"https://x-access-token:${{ secrets.MIRROR_TOKEN }}@github.com/Breadway/bos-settings.git" \
|
||||
'+refs/heads/*:refs/heads/*' '+refs/tags/*:refs/tags/*'
|
||||
40
.forgejo/workflows/package.yml
Normal file
40
.forgejo/workflows/package.yml
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
name: Build and publish package
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ['v*']
|
||||
|
||||
jobs:
|
||||
package:
|
||||
runs-on: [self-hosted, hestia]
|
||||
container:
|
||||
image: archlinux:latest
|
||||
steps:
|
||||
# Note: no actions/checkout — the archlinux image has no Node, which JS
|
||||
# actions require. Everything runs as shell steps and clones manually.
|
||||
- name: Build and publish
|
||||
env:
|
||||
PUBLISH_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
VERSION="${GITHUB_REF_NAME#v}"
|
||||
pacman -Syu --noconfirm base-devel git rust cargo gtk4 glib2
|
||||
useradd -m builder
|
||||
git config --global --add safe.directory '*'
|
||||
git clone --branch "${GITHUB_REF_NAME}" --depth 1 \
|
||||
"https://git.breadway.dev/${GITHUB_REPOSITORY}.git" /home/builder/src
|
||||
cd /home/builder/src
|
||||
git archive --format=tar.gz --prefix="bos-settings-${VERSION}/" HEAD \
|
||||
> packaging/bos-settings-${VERSION}.tar.gz
|
||||
SHA=$(sha256sum packaging/bos-settings-${VERSION}.tar.gz | awk '{print $1}')
|
||||
sed -i "s/^pkgver=.*/pkgver=${VERSION}/" packaging/PKGBUILD
|
||||
sed -i "s/^sha256sums=.*/sha256sums=('${SHA}')/" packaging/PKGBUILD
|
||||
chown -R builder:builder /home/builder/src
|
||||
# --nocheck: packaging builds the artifact; tests belong in a CI job.
|
||||
su builder -c "cd /home/builder/src/packaging && makepkg -f --noconfirm --nocheck"
|
||||
PKG=$(find /home/builder/src/packaging -name '*.pkg.tar.zst' | head -1)
|
||||
curl -fsS -X PUT \
|
||||
-H "Authorization: token ${PUBLISH_TOKEN}" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
--data-binary "@${PKG}" \
|
||||
"https://git.breadway.dev/api/packages/Breadway/arch/os"
|
||||
34
.gitignore
vendored
Normal file
34
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# Rust build artifacts
|
||||
/target/
|
||||
**/*.pdb
|
||||
|
||||
# Editor / IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.direnv/
|
||||
|
||||
# OS artifacts
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
desktop.ini
|
||||
|
||||
# Environment / secrets
|
||||
.env
|
||||
.env.local
|
||||
*.env.*
|
||||
secrets/
|
||||
*.pem
|
||||
*.key
|
||||
*.p12
|
||||
|
||||
# Runtime / logs
|
||||
*.log
|
||||
logs/
|
||||
*.pid
|
||||
*.sock
|
||||
|
||||
# Claude Code local agent state
|
||||
.claude/
|
||||
978
Cargo.lock
generated
Normal file
978
Cargo.lock
generated
Normal file
|
|
@ -0,0 +1,978 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "async-channel"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"event-listener-strategy",
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
|
||||
|
||||
[[package]]
|
||||
name = "bos-settings"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"bread-theme",
|
||||
"glib",
|
||||
"gtk4",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"toml 0.8.23",
|
||||
"toml_edit 0.22.27",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bread-theme"
|
||||
version = "0.2.3"
|
||||
source = "git+https://github.com/Breadway/bread-ecosystem?tag=v0.2.10#17d1bb85801b9a8c195b64c02d288cd662c9c780"
|
||||
dependencies = [
|
||||
"dirs",
|
||||
"gtk4",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cairo-rs"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5cc8d9aa793480744cd9a0524fef1a2e197d9eaa0f739cde19d16aba530dcb95"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cairo-sys-rs",
|
||||
"glib",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cairo-sys-rs"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8b4985713047f5faee02b8db6a6ef32bbb50269ff53c1aee716d1d195b76d54"
|
||||
dependencies = [
|
||||
"glib-sys",
|
||||
"libc",
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-expr"
|
||||
version = "0.20.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb693542bcafa528e198be0ebd9d3632ca5b7c93dbe7237460e199910835997c"
|
||||
dependencies = [
|
||||
"smallvec",
|
||||
"target-lexicon",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "5.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
|
||||
dependencies = [
|
||||
"dirs-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "event-listener"
|
||||
version = "5.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"parking",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event-listener-strategy"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "field-offset"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f"
|
||||
dependencies = [
|
||||
"memoffset",
|
||||
"rustc_version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-macro",
|
||||
"futures-task",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gdk-pixbuf"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25f420376dbee041b2db374ce4573892a36222bb3f6c0c43e24f0d67eae9b646"
|
||||
dependencies = [
|
||||
"gdk-pixbuf-sys",
|
||||
"gio",
|
||||
"glib",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gdk-pixbuf-sys"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48f31b37b1fc4b48b54f6b91b7ef04c18e00b4585d98359dd7b998774bbd91fb"
|
||||
dependencies = [
|
||||
"gio-sys",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"libc",
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gdk4"
|
||||
version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d81e2a6c6ecba2aab60633a98df1868b03fa0bfdce8105edc27c1bccf71f0e39"
|
||||
dependencies = [
|
||||
"cairo-rs",
|
||||
"gdk-pixbuf",
|
||||
"gdk4-sys",
|
||||
"gio",
|
||||
"glib",
|
||||
"libc",
|
||||
"pango",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gdk4-sys"
|
||||
version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d8f608d8d7d229975c4d0d026f5d3071598c4ddab3c5262b0a31840fec78d13"
|
||||
dependencies = [
|
||||
"cairo-sys-rs",
|
||||
"gdk-pixbuf-sys",
|
||||
"gio-sys",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"libc",
|
||||
"pango-sys",
|
||||
"pkg-config",
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gio"
|
||||
version = "0.22.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b3e1f669909c326b9413bde5a742097b8c90a7d78f45326db13668984769ded"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-util",
|
||||
"gio-sys",
|
||||
"glib",
|
||||
"libc",
|
||||
"pin-project-lite",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gio-sys"
|
||||
version = "0.22.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "353fdc7da7cd16da916104b1e0e4e7de380ec9c8aaa20d4d742d66310ab4b0d5"
|
||||
dependencies = [
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"libc",
|
||||
"system-deps",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glib"
|
||||
version = "0.22.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddbcf514bd1881fc1b960e4e52b4e82873f4da3bceddbd58d42827b508888100"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
"gio-sys",
|
||||
"glib-macros",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"libc",
|
||||
"memchr",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glib-macros"
|
||||
version = "0.22.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "506d23499707c7142898429757e8d9a3871d965239a2cb66dfa05052be6d6f19"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glib-sys"
|
||||
version = "0.22.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "030967459f9f676851872c6304adea7825c6d462ec9b72554c733cf0c5952233"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gobject-sys"
|
||||
version = "0.22.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22a861859b887a79cf461359c192c97a57d8fb0229dd291232e57aa11f6fa72c"
|
||||
dependencies = [
|
||||
"glib-sys",
|
||||
"libc",
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "graphene-rs"
|
||||
version = "0.22.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb856b9c558971c3f13ab692358926da710b046932a4e087aedcc35b040d7dff"
|
||||
dependencies = [
|
||||
"glib",
|
||||
"graphene-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "graphene-sys"
|
||||
version = "0.22.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c7ffdfde88f3570d3705e0d8a2433e036d387a1f2930bbf47eafcb5f569fd04"
|
||||
dependencies = [
|
||||
"glib-sys",
|
||||
"libc",
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gsk4"
|
||||
version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b867be1c5f14dcb8f552c0eff6e9a9b1da5f8b43943e8efc3a63c889d84952ff"
|
||||
dependencies = [
|
||||
"cairo-rs",
|
||||
"gdk4",
|
||||
"glib",
|
||||
"graphene-rs",
|
||||
"gsk4-sys",
|
||||
"libc",
|
||||
"pango",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gsk4-sys"
|
||||
version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b7c7eb2e681ee896646cfb8872b431f24d09f53ba9283289d9b10caa6707088"
|
||||
dependencies = [
|
||||
"cairo-sys-rs",
|
||||
"gdk4-sys",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"graphene-sys",
|
||||
"libc",
|
||||
"pango-sys",
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gtk4"
|
||||
version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "98a0a0466484f64b07b5b8184d43fa46be78eb0b8e04ae4e179af31d770b76d9"
|
||||
dependencies = [
|
||||
"cairo-rs",
|
||||
"field-offset",
|
||||
"futures-channel",
|
||||
"gdk-pixbuf",
|
||||
"gdk4",
|
||||
"gio",
|
||||
"glib",
|
||||
"graphene-rs",
|
||||
"gsk4",
|
||||
"gtk4-macros",
|
||||
"gtk4-sys",
|
||||
"libc",
|
||||
"pango",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gtk4-macros"
|
||||
version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ac7179400a36a04de039c24206bb841c5596992b907b43b23ee8d5bdc40d00e"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gtk4-sys"
|
||||
version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82b8f954786af0b1984425c4446b77f5ff6594346181316be3f850caab1c6f01"
|
||||
dependencies = [
|
||||
"cairo-sys-rs",
|
||||
"gdk-pixbuf-sys",
|
||||
"gdk4-sys",
|
||||
"gio-sys",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"graphene-sys",
|
||||
"gsk4-sys",
|
||||
"libc",
|
||||
"pango-sys",
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.186"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "pango"
|
||||
version = "0.22.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d800d8d0de2ad5d0fb046f5344dbaba14a003cf3dd27cc21d85893d35ea316c"
|
||||
dependencies = [
|
||||
"gio",
|
||||
"glib",
|
||||
"pango-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pango-sys"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd111a20ca90fedf03e09c59783c679c00900f1d8491cca5399f5e33609d5d6"
|
||||
dependencies = [
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"libc",
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking"
|
||||
version = "2.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "3.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
|
||||
dependencies = [
|
||||
"toml_edit 0.25.12+spec-1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"libredox",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
|
||||
dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.150"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.118"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-deps"
|
||||
version = "7.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "396a35feb67335377e0251fcbc1092fc85c484bd4e3a7a54319399da127796e7"
|
||||
dependencies = [
|
||||
"cfg-expr",
|
||||
"heck",
|
||||
"pkg-config",
|
||||
"toml 1.1.2+spec-1.1.0",
|
||||
"version-compare",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "target-lexicon"
|
||||
version = "0.13.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned 0.6.9",
|
||||
"toml_datetime 0.6.11",
|
||||
"toml_edit 0.22.27",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "1.1.2+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde_core",
|
||||
"serde_spanned 1.1.1",
|
||||
"toml_datetime 1.1.1+spec-1.1.0",
|
||||
"toml_parser",
|
||||
"toml_writer",
|
||||
"winnow 1.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "1.1.1+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_spanned 0.6.9",
|
||||
"toml_datetime 0.6.11",
|
||||
"toml_write",
|
||||
"winnow 0.7.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.25.12+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"toml_datetime 1.1.1+spec-1.1.0",
|
||||
"toml_parser",
|
||||
"winnow 1.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_parser"
|
||||
version = "1.1.2+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
|
||||
dependencies = [
|
||||
"winnow 1.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_write"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||
|
||||
[[package]]
|
||||
name = "toml_writer"
|
||||
version = "1.1.1+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "version-compare"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2025 Breadway
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
15
README.md
Normal file
15
README.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# bos-settings
|
||||
|
||||
System settings app for [BOS (Bread Operating System)](https://github.com/Breadway/bos) — GTK4, configures every bread\* app's config plus core system settings (network, sound, power, users, firewall, snapshots, packages, AUR, firmware, Hyprland display/appearance/autostart) non-destructively.
|
||||
|
||||
Split out of the `bos` repo into its own repo so a bos-settings release doesn't require a BOS ISO release, and vice versa. Still the only pacman-packaged (not bakery-managed) bread app — see `packaging/README.md`.
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
## Packaging / releasing
|
||||
|
||||
See `packaging/README.md`. In short: bump `Cargo.toml`'s version, tag `vX.Y.Z`, push the tag to both remotes — `.forgejo/workflows/package.yml` builds and publishes to the `[breadway]` pacman repo automatically.
|
||||
|
|
@ -5,7 +5,7 @@ pkgver=0.1.0
|
|||
pkgrel=1
|
||||
pkgdesc="System settings app for Bread OS"
|
||||
arch=('x86_64')
|
||||
url="https://github.com/Breadway/bos"
|
||||
url="https://github.com/Breadway/bos-settings"
|
||||
license=('MIT')
|
||||
# Some Rust deps (ring/mlua) build vendored C/asm into static archives; makepkg's
|
||||
# default -flto=auto emits GCC LTO bitcode the Rust (lld) link cannot read,
|
||||
|
|
@ -21,18 +21,18 @@ sha256sums=('SKIP')
|
|||
|
||||
build() {
|
||||
cd "${srcdir}/${pkgname}-${pkgver}"
|
||||
cargo build --release --locked -p bos-settings
|
||||
cargo build --release --locked
|
||||
}
|
||||
|
||||
check() {
|
||||
cd "${srcdir}/${pkgname}-${pkgver}"
|
||||
cargo test --release --locked -p bos-settings
|
||||
cargo test --release --locked
|
||||
}
|
||||
|
||||
package() {
|
||||
cd "${srcdir}/${pkgname}-${pkgver}"
|
||||
install -Dm755 target/release/bos-settings "${pkgdir}/usr/bin/bos-settings"
|
||||
install -Dm644 packaging/arch/bos-settings.desktop \
|
||||
install -Dm644 packaging/bos-settings.desktop \
|
||||
"${pkgdir}/usr/share/applications/bos-settings.desktop"
|
||||
install -Dm644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
|
||||
}
|
||||
|
|
|
|||
27
src/main.rs
27
src/main.rs
|
|
@ -2,12 +2,37 @@ mod config;
|
|||
mod theme;
|
||||
mod ui;
|
||||
|
||||
use gtk4::gio::ApplicationFlags;
|
||||
use gtk4::prelude::*;
|
||||
|
||||
fn main() {
|
||||
let app = gtk4::Application::builder()
|
||||
.application_id("com.breadway.bos-settings")
|
||||
// HANDLES_COMMAND_LINE: without it, GApplication validates argv
|
||||
// against registered GOptionEntries (none here) and would reject
|
||||
// `--page <id>` with "unknown option" before `activate` ever runs.
|
||||
.flags(ApplicationFlags::HANDLES_COMMAND_LINE)
|
||||
.build();
|
||||
app.connect_activate(ui::window::build_ui);
|
||||
|
||||
app.connect_command_line(|app, cmdline| {
|
||||
let page = parse_page_arg(&cmdline.arguments());
|
||||
ui::window::build_ui(app, page);
|
||||
glib::ExitCode::SUCCESS
|
||||
});
|
||||
|
||||
app.run();
|
||||
}
|
||||
|
||||
/// Best-effort: like any non-command-line-aware launch of an already-running
|
||||
/// GApplication, this is only seen by the launch that becomes primary — a
|
||||
/// `--page` while bos-settings is already open just refocuses the existing
|
||||
/// window on whatever page it was already showing.
|
||||
fn parse_page_arg(args: &[std::ffi::OsString]) -> Option<String> {
|
||||
let mut it = args.iter().skip(1);
|
||||
while let Some(arg) = it.next() {
|
||||
if arg == "--page" {
|
||||
return it.next().and_then(|s| s.to_str()).map(str::to_string);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,11 +37,13 @@ pub const SYSTEM_ITEMS: &[SidebarItem] = &[
|
|||
item("sound", "Sound", "audio-volume-high-symbolic"),
|
||||
item("power", "Power", "battery-good-symbolic"),
|
||||
item("datetime", "Date & Time", "preferences-system-time-symbolic"),
|
||||
item_sub("hyprland", "Display", "hyprland.lua", "video-display-symbolic"),
|
||||
item_sub("hyprland", "Display", "monitors.json", "video-display-symbolic"),
|
||||
item_sub("autostart", "Startup Apps", "autostart.json", "system-run-symbolic"),
|
||||
item("users", "Users", "system-users-symbolic"),
|
||||
];
|
||||
|
||||
pub const PERSONALIZATION_ITEMS: &[SidebarItem] = &[
|
||||
item_sub("appearance", "Appearance", "settings.json", "applications-graphics-symbolic"),
|
||||
item_sub("breadpaper", "Wallpaper", "breadpaper", "preferences-desktop-wallpaper-symbolic"),
|
||||
item_sub("breadbar", "Bar", "breadbar", "view-grid-symbolic"),
|
||||
item_sub("breadbox", "Launcher", "breadbox", "view-app-grid-symbolic"),
|
||||
|
|
|
|||
224
src/ui/views/appearance.rs
Normal file
224
src/ui/views/appearance.rs
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
//! hypr/settings.json — Hyprland gaps/borders/blur/shadow/input, read by
|
||||
//! `scripts/ui/settings.lua` on the Hyprland side (see hyprland.lua). Unlike
|
||||
//! the TOML-backed views, this has no comments to preserve, so it's modeled
|
||||
//! as a plain typed struct (round-tripped whole) rather than the
|
||||
//! `toml_edit`/`Doc` path-based editor the other panels use.
|
||||
//!
|
||||
//! `Default` here must stay in sync with `scripts/ui/settings.lua`'s
|
||||
//! `DEFAULTS` table on the Hyprland side — both independently define "what
|
||||
//! BOS ships out of the box," the same duplication `binds.json`/
|
||||
//! `content/keybinds.rs` already accept for the same reason (two different
|
||||
//! languages/processes reading the same file, neither able to import the
|
||||
//! other's defaults).
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use gtk4::prelude::*;
|
||||
use gtk4::{
|
||||
Adjustment, Box as GBox, ColorDialog, ColorDialogButton, DropDown, Entry, Expression, Orientation,
|
||||
SpinButton, StringList, Switch,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::ui::widgets as w;
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
struct Appearance {
|
||||
gaps_in: i64,
|
||||
gaps_out: i64,
|
||||
border_size: i64,
|
||||
active_border: String,
|
||||
inactive_border: String,
|
||||
layout: String,
|
||||
resize_on_border: bool,
|
||||
rounding: i64,
|
||||
blur_enabled: bool,
|
||||
blur_size: i64,
|
||||
blur_passes: i64,
|
||||
shadow_enabled: bool,
|
||||
shadow_range: i64,
|
||||
shadow_render_power: i64,
|
||||
kb_layout: String,
|
||||
follow_mouse: i64,
|
||||
natural_scroll: bool,
|
||||
}
|
||||
|
||||
impl Default for Appearance {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
gaps_in: 5,
|
||||
gaps_out: 10,
|
||||
border_size: 2,
|
||||
active_border: "rgba(88c0d0ff)".to_string(),
|
||||
inactive_border: "rgba(4c566aff)".to_string(),
|
||||
layout: "dwindle".to_string(),
|
||||
resize_on_border: true,
|
||||
rounding: 8,
|
||||
blur_enabled: true,
|
||||
blur_size: 6,
|
||||
blur_passes: 2,
|
||||
shadow_enabled: true,
|
||||
shadow_range: 12,
|
||||
shadow_render_power: 3,
|
||||
kb_layout: "us".to_string(),
|
||||
follow_mouse: 1,
|
||||
natural_scroll: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn config_path() -> std::path::PathBuf {
|
||||
crate::config::config_dir().join("hypr/settings.json")
|
||||
}
|
||||
|
||||
/// A missing or malformed file yields defaults — same failsafe posture as
|
||||
/// the Lua loader reading this same file on the Hyprland side.
|
||||
fn load() -> Appearance {
|
||||
std::fs::read_to_string(config_path()).ok().and_then(|s| serde_json::from_str(&s).ok()).unwrap_or_default()
|
||||
}
|
||||
|
||||
fn save(a: &Appearance) -> std::io::Result<()> {
|
||||
let path = config_path();
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
std::fs::write(path, serde_json::to_string_pretty(a).unwrap_or_default())
|
||||
}
|
||||
|
||||
/// "rgba(RRGGBBAA)" (Hyprland's format) <-> gdk::RGBA, so the color fields
|
||||
/// get a real color-picker button instead of a raw hex text field.
|
||||
fn parse_hypr_color(s: &str) -> Option<gtk4::gdk::RGBA> {
|
||||
let inner = s.strip_prefix("rgba(")?.strip_suffix(')')?;
|
||||
if inner.len() != 8 {
|
||||
return None;
|
||||
}
|
||||
let r = u8::from_str_radix(&inner[0..2], 16).ok()?;
|
||||
let g = u8::from_str_radix(&inner[2..4], 16).ok()?;
|
||||
let b = u8::from_str_radix(&inner[4..6], 16).ok()?;
|
||||
let a = u8::from_str_radix(&inner[6..8], 16).ok()?;
|
||||
Some(gtk4::gdk::RGBA::new(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, a as f32 / 255.0))
|
||||
}
|
||||
|
||||
fn to_hypr_color(c: >k4::gdk::RGBA) -> String {
|
||||
let clamp = |v: f32| (v.clamp(0.0, 1.0) * 255.0).round() as u8;
|
||||
format!("rgba({:02x}{:02x}{:02x}{:02x})", clamp(c.red()), clamp(c.green()), clamp(c.blue()), clamp(c.alpha()))
|
||||
}
|
||||
|
||||
fn color_row(label: &str, model: &Rc<RefCell<Appearance>>, get: fn(&Appearance) -> &str, set: fn(&mut Appearance, String)) -> GBox {
|
||||
let cur = parse_hypr_color(get(&model.borrow())).unwrap_or(gtk4::gdk::RGBA::new(0.5, 0.5, 0.5, 1.0));
|
||||
let btn = ColorDialogButton::new(Some(ColorDialog::builder().with_alpha(true).build()));
|
||||
btn.set_rgba(&cur);
|
||||
let model = model.clone();
|
||||
btn.connect_rgba_notify(move |b| {
|
||||
set(&mut model.borrow_mut(), to_hypr_color(&b.rgba()));
|
||||
});
|
||||
w::row(label, &btn)
|
||||
}
|
||||
|
||||
fn spin_row(label: &str, model: &Rc<RefCell<Appearance>>, min: f64, max: f64, get: fn(&Appearance) -> i64, set: fn(&mut Appearance, i64)) -> GBox {
|
||||
let cur = get(&model.borrow());
|
||||
let adj = Adjustment::new(cur as f64, min, max, 1.0, 1.0, 0.0);
|
||||
let spin = SpinButton::new(Some(&adj), 1.0, 0);
|
||||
let model = model.clone();
|
||||
spin.connect_value_changed(move |s| set(&mut model.borrow_mut(), s.value() as i64));
|
||||
w::row(label, &spin)
|
||||
}
|
||||
|
||||
fn switch_row(label: &str, model: &Rc<RefCell<Appearance>>, get: fn(&Appearance) -> bool, set: fn(&mut Appearance, bool)) -> GBox {
|
||||
let sw = Switch::new();
|
||||
sw.set_active(get(&model.borrow()));
|
||||
let model = model.clone();
|
||||
sw.connect_active_notify(move |s| set(&mut model.borrow_mut(), s.is_active()));
|
||||
w::row(label, &sw)
|
||||
}
|
||||
|
||||
fn entry_row(label: &str, model: &Rc<RefCell<Appearance>>, get: fn(&Appearance) -> String, set: fn(&mut Appearance, String)) -> GBox {
|
||||
let entry = Entry::new();
|
||||
entry.set_text(&get(&model.borrow()));
|
||||
entry.set_hexpand(true);
|
||||
entry.set_width_chars(16);
|
||||
let model = model.clone();
|
||||
entry.connect_changed(move |e| set(&mut model.borrow_mut(), e.text().to_string()));
|
||||
w::row(label, &entry)
|
||||
}
|
||||
|
||||
fn dropdown_row(label: &str, model: &Rc<RefCell<Appearance>>, options: &[&str], get: fn(&Appearance) -> &str, set: fn(&mut Appearance, String)) -> GBox {
|
||||
let cur = get(&model.borrow()).to_string();
|
||||
let dd = DropDown::new(Some(StringList::new(options)), Expression::NONE);
|
||||
dd.set_selected(options.iter().position(|o| *o == cur).unwrap_or(0) as u32);
|
||||
let owned: Vec<String> = options.iter().map(|s| s.to_string()).collect();
|
||||
let model = model.clone();
|
||||
dd.connect_selected_notify(move |dd| {
|
||||
if let Some(opt) = owned.get(dd.selected() as usize) {
|
||||
set(&mut model.borrow_mut(), opt.clone());
|
||||
}
|
||||
});
|
||||
w::row(label, &dd)
|
||||
}
|
||||
|
||||
pub fn build() -> GBox {
|
||||
let (outer, content) = w::view_scaffold("Appearance");
|
||||
|
||||
content.append(&w::hint(
|
||||
"Gaps, borders, blur, and input feel for Hyprland — the same \
|
||||
settings.json the compositor itself reads at login.",
|
||||
));
|
||||
|
||||
let model = Rc::new(RefCell::new(load()));
|
||||
|
||||
content.append(&w::section("Layout & borders"));
|
||||
content.append(&spin_row("Gaps (inner)", &model, 0.0, 50.0, |a| a.gaps_in, |a, v| a.gaps_in = v));
|
||||
content.append(&spin_row("Gaps (outer)", &model, 0.0, 50.0, |a| a.gaps_out, |a, v| a.gaps_out = v));
|
||||
content.append(&spin_row("Border width", &model, 0.0, 10.0, |a| a.border_size, |a, v| a.border_size = v));
|
||||
content.append(&color_row("Active border color", &model, |a| &a.active_border, |a, v| a.active_border = v));
|
||||
content.append(&color_row("Inactive border color", &model, |a| &a.inactive_border, |a, v| a.inactive_border = v));
|
||||
content.append(&dropdown_row("Tiling layout", &model, &["dwindle", "master"], |a| &a.layout, |a, v| a.layout = v));
|
||||
content.append(&switch_row("Resize by dragging borders", &model, |a| a.resize_on_border, |a, v| a.resize_on_border = v));
|
||||
|
||||
content.append(&w::section("Effects"));
|
||||
content.append(&spin_row("Corner rounding", &model, 0.0, 30.0, |a| a.rounding, |a, v| a.rounding = v));
|
||||
content.append(&switch_row("Blur", &model, |a| a.blur_enabled, |a, v| a.blur_enabled = v));
|
||||
content.append(&spin_row("Blur size", &model, 0.0, 20.0, |a| a.blur_size, |a, v| a.blur_size = v));
|
||||
content.append(&spin_row("Blur passes", &model, 1.0, 5.0, |a| a.blur_passes, |a, v| a.blur_passes = v));
|
||||
content.append(&switch_row("Window shadows", &model, |a| a.shadow_enabled, |a, v| a.shadow_enabled = v));
|
||||
content.append(&spin_row("Shadow range", &model, 0.0, 40.0, |a| a.shadow_range, |a, v| a.shadow_range = v));
|
||||
content.append(&spin_row("Shadow render power", &model, 1.0, 4.0, |a| a.shadow_render_power, |a, v| a.shadow_render_power = v));
|
||||
|
||||
content.append(&w::section("Input"));
|
||||
content.append(&entry_row("Keyboard layout", &model, |a| a.kb_layout.clone(), |a, v| a.kb_layout = v));
|
||||
let follow_mouse_row = spin_row("Focus-follows-mouse mode", &model, 0.0, 3.0, |a| a.follow_mouse, |a, v| a.follow_mouse = v);
|
||||
content.append(&follow_mouse_row);
|
||||
content.append(&w::hint("0-3 — see the Hyprland wiki's follow_mouse setting for exact behavior of each value."));
|
||||
content.append(&switch_row("Natural scrolling (touchpad)", &model, |a| a.natural_scroll, |a, v| a.natural_scroll = v));
|
||||
|
||||
content.append(&w::hint("Applies on next login/Hyprland reload — this saves settings.json, it doesn't reload Hyprland live."));
|
||||
|
||||
let btn_row = GBox::new(Orientation::Horizontal, 12);
|
||||
btn_row.set_margin_top(16);
|
||||
let save_btn = gtk4::Button::with_label("Save");
|
||||
save_btn.add_css_class("suggested-action");
|
||||
let status = gtk4::Label::new(None);
|
||||
status.add_css_class("dim-label");
|
||||
{
|
||||
let model = model.clone();
|
||||
let status = status.clone();
|
||||
save_btn.connect_clicked(move |_| match save(&model.borrow()) {
|
||||
Ok(()) => {
|
||||
status.set_text("Saved");
|
||||
let lbl = status.clone();
|
||||
glib::timeout_add_seconds_local(3, move || {
|
||||
lbl.set_text("");
|
||||
glib::ControlFlow::Break
|
||||
});
|
||||
}
|
||||
Err(e) => status.set_text(&format!("Error: {e}")),
|
||||
});
|
||||
}
|
||||
btn_row.append(&save_btn);
|
||||
btn_row.append(&status);
|
||||
outer.append(&btn_row);
|
||||
|
||||
outer
|
||||
}
|
||||
201
src/ui/views/autostart.rs
Normal file
201
src/ui/views/autostart.rs
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
//! hypr/autostart.json — the *extra*, user-toggleable autostart apps
|
||||
//! (breadbar, hypridle, bos-netcheck, breadhelp, plus anything a user adds).
|
||||
//! The core bootstrap sequence (theme generation, dark-mode gsettings,
|
||||
//! polkit agent, wallpaper daemon, breadd's Wayland-env fix, breadclipd)
|
||||
//! stays hardcoded in hyprland.lua on purpose — it's timing/order-sensitive
|
||||
//! infrastructure, not something this panel exposes.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use gtk4::prelude::*;
|
||||
use gtk4::{Box as GBox, Button, Entry, Label, ListBox, ListBoxRow, Orientation, Switch};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::ui::widgets as w;
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
struct Entry_ {
|
||||
command: String,
|
||||
#[serde(default)]
|
||||
label: String,
|
||||
#[serde(default = "default_true")]
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct AutostartFile {
|
||||
#[serde(default)]
|
||||
extra: Vec<Entry_>,
|
||||
}
|
||||
|
||||
fn default_extra() -> Vec<Entry_> {
|
||||
vec![
|
||||
Entry_ { command: "breadbar".to_string(), label: "Bar (breadbar)".to_string(), enabled: true },
|
||||
Entry_ { command: "hypridle".to_string(), label: "Idle / lock daemon (hypridle)".to_string(), enabled: true },
|
||||
Entry_ { command: "bos-netcheck".to_string(), label: "Network connectivity check".to_string(), enabled: true },
|
||||
Entry_ { command: "breadhelp --autostart".to_string(), label: "BOS Help (first-run onboarding)".to_string(), enabled: true },
|
||||
]
|
||||
}
|
||||
|
||||
fn config_path() -> std::path::PathBuf {
|
||||
crate::config::config_dir().join("hypr/autostart.json")
|
||||
}
|
||||
|
||||
fn load() -> Vec<Entry_> {
|
||||
std::fs::read_to_string(config_path())
|
||||
.ok()
|
||||
.and_then(|s| serde_json::from_str::<AutostartFile>(&s).ok())
|
||||
.map(|f| f.extra)
|
||||
.unwrap_or_else(default_extra)
|
||||
}
|
||||
|
||||
fn save(entries: &[Entry_]) -> std::io::Result<()> {
|
||||
let path = config_path();
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
let file = AutostartFile { extra: entries.to_vec() };
|
||||
std::fs::write(path, serde_json::to_string_pretty(&file).unwrap_or_default())
|
||||
}
|
||||
|
||||
fn rebuild(list: &ListBox, model: &Rc<RefCell<Vec<Entry_>>>) {
|
||||
while let Some(child) = list.first_child() {
|
||||
list.remove(&child);
|
||||
}
|
||||
for (i, entry) in model.borrow().iter().enumerate() {
|
||||
let row = ListBoxRow::new();
|
||||
row.set_selectable(false);
|
||||
let hbox = GBox::new(Orientation::Horizontal, 8);
|
||||
hbox.set_margin_top(6);
|
||||
hbox.set_margin_bottom(6);
|
||||
hbox.set_margin_start(8);
|
||||
hbox.set_margin_end(8);
|
||||
|
||||
let enabled = Switch::new();
|
||||
enabled.set_active(entry.enabled);
|
||||
enabled.set_valign(gtk4::Align::Center);
|
||||
|
||||
let label = Entry::new();
|
||||
label.set_text(&entry.label);
|
||||
label.set_placeholder_text(Some("Label"));
|
||||
label.set_width_chars(20);
|
||||
|
||||
let command = Entry::new();
|
||||
command.set_text(&entry.command);
|
||||
command.set_placeholder_text(Some("command"));
|
||||
command.set_hexpand(true);
|
||||
|
||||
let remove = Button::with_label("Remove");
|
||||
remove.add_css_class("destructive-action");
|
||||
|
||||
{
|
||||
let model = model.clone();
|
||||
enabled.connect_active_notify(move |s| {
|
||||
if let Some(e) = model.borrow_mut().get_mut(i) {
|
||||
e.enabled = s.is_active();
|
||||
}
|
||||
});
|
||||
}
|
||||
{
|
||||
let model = model.clone();
|
||||
label.connect_changed(move |e| {
|
||||
if let Some(entry) = model.borrow_mut().get_mut(i) {
|
||||
entry.label = e.text().to_string();
|
||||
}
|
||||
});
|
||||
}
|
||||
{
|
||||
let model = model.clone();
|
||||
command.connect_changed(move |e| {
|
||||
if let Some(entry) = model.borrow_mut().get_mut(i) {
|
||||
entry.command = e.text().to_string();
|
||||
}
|
||||
});
|
||||
}
|
||||
{
|
||||
let model = model.clone();
|
||||
let list = list.clone();
|
||||
remove.connect_clicked(move |_| {
|
||||
model.borrow_mut().remove(i);
|
||||
rebuild(&list, &model);
|
||||
});
|
||||
}
|
||||
|
||||
hbox.append(&enabled);
|
||||
hbox.append(&label);
|
||||
hbox.append(&command);
|
||||
hbox.append(&remove);
|
||||
row.set_child(Some(&hbox));
|
||||
list.append(&row);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build() -> GBox {
|
||||
let (outer, content) = w::view_scaffold("Startup Apps");
|
||||
|
||||
content.append(&w::hint(
|
||||
"What launches after login, beyond the core desktop (bar, theme, \
|
||||
clipboard, etc. always start regardless). Toggle off, edit, or add \
|
||||
your own — each row is one command run at login.",
|
||||
));
|
||||
|
||||
let model = Rc::new(RefCell::new(load()));
|
||||
|
||||
content.append(&w::section("Extra autostart apps"));
|
||||
let list = ListBox::new();
|
||||
list.set_selection_mode(gtk4::SelectionMode::None);
|
||||
list.add_css_class("boxed-list");
|
||||
rebuild(&list, &model);
|
||||
content.append(&list);
|
||||
|
||||
let add_btn = Button::with_label("Add app");
|
||||
add_btn.set_halign(gtk4::Align::Start);
|
||||
add_btn.set_margin_top(6);
|
||||
{
|
||||
let model = model.clone();
|
||||
let list = list.clone();
|
||||
add_btn.connect_clicked(move |_| {
|
||||
model.borrow_mut().push(Entry_ { command: String::new(), label: String::new(), enabled: true });
|
||||
rebuild(&list, &model);
|
||||
});
|
||||
}
|
||||
content.append(&add_btn);
|
||||
|
||||
let btn_row = GBox::new(Orientation::Horizontal, 12);
|
||||
btn_row.set_margin_top(16);
|
||||
let save_btn = Button::with_label("Save");
|
||||
save_btn.add_css_class("suggested-action");
|
||||
let status = Label::new(None);
|
||||
status.add_css_class("dim-label");
|
||||
{
|
||||
let model = model.clone();
|
||||
let status = status.clone();
|
||||
save_btn.connect_clicked(move |_| {
|
||||
// Empty command rows (still-being-typed "Add app" entries) are
|
||||
// dropped on save rather than written as a broken autostart.json
|
||||
// entry the Lua loader would otherwise have to reject.
|
||||
let entries: Vec<Entry_> = model.borrow().iter().filter(|e| !e.command.trim().is_empty()).cloned().collect();
|
||||
match save(&entries) {
|
||||
Ok(()) => {
|
||||
status.set_text("Saved");
|
||||
let lbl = status.clone();
|
||||
glib::timeout_add_seconds_local(3, move || {
|
||||
lbl.set_text("");
|
||||
glib::ControlFlow::Break
|
||||
});
|
||||
}
|
||||
Err(e) => status.set_text(&format!("Error: {e}")),
|
||||
}
|
||||
});
|
||||
}
|
||||
btn_row.append(&save_btn);
|
||||
btn_row.append(&status);
|
||||
outer.append(&btn_row);
|
||||
|
||||
outer
|
||||
}
|
||||
|
|
@ -1,11 +1,78 @@
|
|||
//! Display: live-connected-monitor readout (from `hyprctl monitors -j`) plus
|
||||
//! a real editor for `hypr/monitors.json` — the monitor *layout* Hyprland
|
||||
//! itself reads at login via `scripts/display/monitors.lua`. Like
|
||||
//! appearance.rs/autostart.rs, this is a plain typed struct round-tripped
|
||||
//! whole (JSON has no comments to preserve), not the TOML `Doc` pattern.
|
||||
//!
|
||||
//! `Default` here (the single wildcard rule) must stay in sync with
|
||||
//! `scripts/display/monitors.lua`'s own `DEFAULT_MONITORS` fallback.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use gtk4::prelude::*;
|
||||
use gtk4::{Box as GBox, Button};
|
||||
use std::process::Command;
|
||||
use gtk4::{Box as GBox, Button, Entry, Label, ListBox, ListBoxRow, Orientation};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::ui::widgets as w;
|
||||
|
||||
fn get_monitors() -> Vec<(String, String)> {
|
||||
let Ok(output) = Command::new("hyprctl").args(["monitors", "-j"]).output() else {
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
struct MonitorRule {
|
||||
output: String,
|
||||
#[serde(default = "default_mode")]
|
||||
mode: String,
|
||||
#[serde(default = "default_position")]
|
||||
position: String,
|
||||
#[serde(default = "default_scale")]
|
||||
scale: String,
|
||||
}
|
||||
|
||||
fn default_mode() -> String {
|
||||
"preferred".to_string()
|
||||
}
|
||||
fn default_position() -> String {
|
||||
"auto".to_string()
|
||||
}
|
||||
fn default_scale() -> String {
|
||||
"auto".to_string()
|
||||
}
|
||||
|
||||
impl Default for MonitorRule {
|
||||
fn default() -> Self {
|
||||
Self { output: String::new(), mode: default_mode(), position: default_position(), scale: default_scale() }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct MonitorsFile {
|
||||
#[serde(default)]
|
||||
monitors: Vec<MonitorRule>,
|
||||
}
|
||||
|
||||
fn config_path() -> std::path::PathBuf {
|
||||
crate::config::config_dir().join("hypr/monitors.json")
|
||||
}
|
||||
|
||||
fn load() -> Vec<MonitorRule> {
|
||||
std::fs::read_to_string(config_path())
|
||||
.ok()
|
||||
.and_then(|s| serde_json::from_str::<MonitorsFile>(&s).ok())
|
||||
.filter(|f| !f.monitors.is_empty())
|
||||
.map(|f| f.monitors)
|
||||
.unwrap_or_else(|| vec![MonitorRule::default()])
|
||||
}
|
||||
|
||||
fn save(rules: &[MonitorRule]) -> std::io::Result<()> {
|
||||
let path = config_path();
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
let file = MonitorsFile { monitors: rules.to_vec() };
|
||||
std::fs::write(path, serde_json::to_string_pretty(&file).unwrap_or_default())
|
||||
}
|
||||
|
||||
fn get_live_monitors() -> Vec<(String, String)> {
|
||||
let Ok(output) = std::process::Command::new("hyprctl").args(["monitors", "-j"]).output() else {
|
||||
return Vec::new();
|
||||
};
|
||||
let text = String::from_utf8_lossy(&output.stdout);
|
||||
|
|
@ -34,16 +101,111 @@ fn hypr_path(name: &str) -> std::path::PathBuf {
|
|||
/// wrapper. Uses kitty, which is what BOS actually ships (not foot).
|
||||
fn open_in_terminal(path: &std::path::Path) {
|
||||
let editor = std::env::var("EDITOR").unwrap_or_else(|_| "nano".to_string());
|
||||
if let Ok(mut child) = Command::new("kitty").args(["-e", &editor]).arg(path).spawn() {
|
||||
if let Ok(mut child) = std::process::Command::new("kitty").args(["-e", &editor]).arg(path).spawn() {
|
||||
std::thread::spawn(move || { let _ = child.wait(); });
|
||||
}
|
||||
}
|
||||
|
||||
fn rebuild(list: &ListBox, model: &Rc<RefCell<Vec<MonitorRule>>>) {
|
||||
while let Some(child) = list.first_child() {
|
||||
list.remove(&child);
|
||||
}
|
||||
for (i, rule) in model.borrow().iter().enumerate() {
|
||||
let row = ListBoxRow::new();
|
||||
row.set_selectable(false);
|
||||
let hbox = GBox::new(Orientation::Horizontal, 8);
|
||||
hbox.set_margin_top(6);
|
||||
hbox.set_margin_bottom(6);
|
||||
hbox.set_margin_start(8);
|
||||
hbox.set_margin_end(8);
|
||||
|
||||
let output = Entry::new();
|
||||
output.set_text(&rule.output);
|
||||
output.set_placeholder_text(Some("any (blank = all)"));
|
||||
output.set_width_chars(12);
|
||||
|
||||
let mode = Entry::new();
|
||||
mode.set_text(&rule.mode);
|
||||
mode.set_placeholder_text(Some("preferred / 1920x1080@60"));
|
||||
mode.set_width_chars(18);
|
||||
|
||||
let position = Entry::new();
|
||||
position.set_text(&rule.position);
|
||||
position.set_placeholder_text(Some("auto / 0x0"));
|
||||
position.set_width_chars(10);
|
||||
|
||||
let scale = Entry::new();
|
||||
scale.set_text(&rule.scale);
|
||||
scale.set_placeholder_text(Some("auto / 1"));
|
||||
scale.set_width_chars(8);
|
||||
scale.set_hexpand(true);
|
||||
|
||||
let remove = Button::with_label("Remove");
|
||||
remove.add_css_class("destructive-action");
|
||||
|
||||
{
|
||||
let model = model.clone();
|
||||
output.connect_changed(move |e| {
|
||||
if let Some(r) = model.borrow_mut().get_mut(i) {
|
||||
r.output = e.text().to_string();
|
||||
}
|
||||
});
|
||||
}
|
||||
{
|
||||
let model = model.clone();
|
||||
mode.connect_changed(move |e| {
|
||||
if let Some(r) = model.borrow_mut().get_mut(i) {
|
||||
r.mode = e.text().to_string();
|
||||
}
|
||||
});
|
||||
}
|
||||
{
|
||||
let model = model.clone();
|
||||
position.connect_changed(move |e| {
|
||||
if let Some(r) = model.borrow_mut().get_mut(i) {
|
||||
r.position = e.text().to_string();
|
||||
}
|
||||
});
|
||||
}
|
||||
{
|
||||
let model = model.clone();
|
||||
scale.connect_changed(move |e| {
|
||||
if let Some(r) = model.borrow_mut().get_mut(i) {
|
||||
r.scale = e.text().to_string();
|
||||
}
|
||||
});
|
||||
}
|
||||
{
|
||||
let model = model.clone();
|
||||
let list = list.clone();
|
||||
remove.connect_clicked(move |_| {
|
||||
model.borrow_mut().remove(i);
|
||||
if model.borrow().is_empty() {
|
||||
model.borrow_mut().push(MonitorRule::default());
|
||||
}
|
||||
rebuild(&list, &model);
|
||||
});
|
||||
}
|
||||
|
||||
hbox.append(&Label::new(Some("Output")));
|
||||
hbox.append(&output);
|
||||
hbox.append(&Label::new(Some("Mode")));
|
||||
hbox.append(&mode);
|
||||
hbox.append(&Label::new(Some("Position")));
|
||||
hbox.append(&position);
|
||||
hbox.append(&Label::new(Some("Scale")));
|
||||
hbox.append(&scale);
|
||||
hbox.append(&remove);
|
||||
row.set_child(Some(&hbox));
|
||||
list.append(&row);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build() -> GBox {
|
||||
let (outer, content) = w::view_scaffold("Display");
|
||||
|
||||
content.append(&w::section("Connected monitors"));
|
||||
let monitors = get_monitors();
|
||||
let monitors = get_live_monitors();
|
||||
if monitors.is_empty() {
|
||||
content.append(&w::hint("No monitors detected (is Hyprland running?)"));
|
||||
} else {
|
||||
|
|
@ -52,16 +214,58 @@ pub fn build() -> GBox {
|
|||
}
|
||||
}
|
||||
|
||||
content.append(&w::section("Configuration"));
|
||||
content.append(&w::section("Layout"));
|
||||
content.append(&w::hint(
|
||||
"Monitor layout, keyboard/input, and workspace rules are configured \
|
||||
directly in hyprland.lua — there's no live editor for them here yet.",
|
||||
"One row per monitor rule. Leave Output blank to match any monitor \
|
||||
(the default — works on any hardware). Applies on next login/reload.",
|
||||
));
|
||||
|
||||
// BOS's Hyprland config is Lua-native (hyprland.lua), not the classic
|
||||
// hyprland.conf/keybinds.conf pair — those names only ever matched a
|
||||
// stale, unshipped dotfiles/ directory, so this button opened (or
|
||||
// silently created) the wrong file entirely.
|
||||
let model = Rc::new(RefCell::new(load()));
|
||||
let list = ListBox::new();
|
||||
list.set_selection_mode(gtk4::SelectionMode::None);
|
||||
list.add_css_class("boxed-list");
|
||||
rebuild(&list, &model);
|
||||
content.append(&list);
|
||||
|
||||
let add_btn = Button::with_label("Add monitor rule");
|
||||
add_btn.set_halign(gtk4::Align::Start);
|
||||
add_btn.set_margin_top(6);
|
||||
{
|
||||
let model = model.clone();
|
||||
let list = list.clone();
|
||||
add_btn.connect_clicked(move |_| {
|
||||
model.borrow_mut().push(MonitorRule::default());
|
||||
rebuild(&list, &model);
|
||||
});
|
||||
}
|
||||
content.append(&add_btn);
|
||||
|
||||
let btn_row = GBox::new(Orientation::Horizontal, 12);
|
||||
btn_row.set_margin_top(16);
|
||||
let save_btn = Button::with_label("Save");
|
||||
save_btn.add_css_class("suggested-action");
|
||||
let status = Label::new(None);
|
||||
status.add_css_class("dim-label");
|
||||
{
|
||||
let model = model.clone();
|
||||
let status = status.clone();
|
||||
save_btn.connect_clicked(move |_| match save(&model.borrow()) {
|
||||
Ok(()) => {
|
||||
status.set_text("Saved");
|
||||
let lbl = status.clone();
|
||||
glib::timeout_add_seconds_local(3, move || {
|
||||
lbl.set_text("");
|
||||
glib::ControlFlow::Break
|
||||
});
|
||||
}
|
||||
Err(e) => status.set_text(&format!("Error: {e}")),
|
||||
});
|
||||
}
|
||||
btn_row.append(&save_btn);
|
||||
btn_row.append(&status);
|
||||
outer.append(&btn_row);
|
||||
|
||||
content.append(&w::section("Advanced"));
|
||||
let open_btn = Button::with_label("Open hyprland.lua in editor");
|
||||
open_btn.set_halign(gtk4::Align::Start);
|
||||
{
|
||||
|
|
@ -70,15 +274,14 @@ pub fn build() -> GBox {
|
|||
}
|
||||
content.append(&open_btn);
|
||||
|
||||
// Keybinds are defined inline in hyprland.lua (no separate file); point
|
||||
// this at the shipped cheat sheet instead of a keybinds.conf that has
|
||||
// never existed on BOS.
|
||||
let keybinds_btn = Button::with_label("View keybinds cheat sheet");
|
||||
// breadhelp is the real keybind viewer now (SUPER+/) — /usr/share/bos/
|
||||
// keybinds.txt was deleted when breadhelp replaced bos-keybinds/
|
||||
// bos-welcome, so this used to open a file that no longer exists.
|
||||
let keybinds_btn = Button::with_label("View keybinds (breadhelp)");
|
||||
keybinds_btn.set_halign(gtk4::Align::Start);
|
||||
{
|
||||
let kb_path = std::path::PathBuf::from("/usr/share/bos/keybinds.txt");
|
||||
keybinds_btn.connect_clicked(move |_| open_in_terminal(&kb_path));
|
||||
}
|
||||
keybinds_btn.connect_clicked(move |_| {
|
||||
let _ = std::process::Command::new("breadhelp").spawn();
|
||||
});
|
||||
content.append(&keybinds_btn);
|
||||
|
||||
outer
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
pub mod about;
|
||||
pub mod appearance;
|
||||
pub mod aur;
|
||||
pub mod autostart;
|
||||
pub mod bread;
|
||||
pub mod breadbar;
|
||||
pub mod breadbox;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ use super::views;
|
|||
// initial selection, so the two can't independently drift out of sync.
|
||||
const DEFAULT_PAGE: &str = "about";
|
||||
|
||||
pub fn build_ui(app: &Application) {
|
||||
/// `requested_page` comes from `--page <id>` (e.g. breadhelp's "Learn more"
|
||||
/// deep links) and overrides `DEFAULT_PAGE` when it names a real page.
|
||||
pub fn build_ui(app: &Application, requested_page: Option<String>) {
|
||||
let window = ApplicationWindow::builder()
|
||||
.application(app)
|
||||
.title("BOS Settings")
|
||||
|
|
@ -25,7 +27,8 @@ pub fn build_ui(app: &Application) {
|
|||
hpaned.set_shrink_start_child(false);
|
||||
hpaned.set_resize_start_child(false);
|
||||
|
||||
let (sidebar_box, list) = sidebar::build(DEFAULT_PAGE);
|
||||
let initial_page = requested_page.as_deref().unwrap_or(DEFAULT_PAGE);
|
||||
let (sidebar_box, list) = sidebar::build(initial_page);
|
||||
|
||||
let stack = Stack::new();
|
||||
stack.set_hexpand(true);
|
||||
|
|
@ -51,8 +54,10 @@ pub fn build_ui(app: &Application) {
|
|||
stack.add_named(&views::breadpaper::build(), Some("breadpaper"));
|
||||
stack.add_named(&views::breadsearch::build(), Some("breadsearch"));
|
||||
stack.add_named(&views::hyprland::build(), Some("hyprland"));
|
||||
stack.add_named(&views::appearance::build(), Some("appearance"));
|
||||
stack.add_named(&views::autostart::build(), Some("autostart"));
|
||||
|
||||
stack.set_visible_child_name(DEFAULT_PAGE);
|
||||
stack.set_visible_child_name(initial_page);
|
||||
|
||||
{
|
||||
let stack = stack.clone();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue