5 Commits
Author SHA1 Message Date
Bendik Aagaard LynghaugandClaude Fable 5.1 bf16d427b8 release: cargo-release config for tag-driven releases
`cargo release patch --execute` now does what the last three releases
did by hand: bump Cargo.toml/Cargo.lock, rewrite PKGBUILD's pkgver,
stamp the "## Unreleased" changelog section with version and date,
commit as "release X.Y.Z", tag vX.Y.Z and push commit and tag to the
gitea remote (not `origin`, which is the GitHub upstream). The tag then
triggers .gitea/workflows/release.yml as before. No crates.io publish.

CHANGELOG.md gets a standing "## Unreleased" section to collect entries
between releases; README documents the flow.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-07 15:48:45 +02:00
Bendik Aagaard LynghaugandClaude Fable 5.1 4f055935d4 backlight: run without a backlight device instead of panicking (0.4.3)
Release / package (x86_64, /var/local/cargo-target/iris-cargo-home, bare, /usr/bin/sccache, /var/local/rustup, /var/local/cargo-target) (push) Successful in 43s
Release / package (aarch64, , aarch64, , , ) (push) Successful in 2m20s
A config carrying a !backlight widget on a machine with nothing under
/sys/class/backlight (a laptop config on a desktop) hit
expect("did not find backlight device") in Backlight::new, which with
panic = "abort" killed wldash before a window appeared.

The device path is now Option<PathBuf>. A missing device, or a
configured `device` name that does not exist, prints one line on stderr
and leaves the widget in place so layout indices stay aligned: it
renders empty, skips sysfs reads and writes, and ignores clicks.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-05 13:54:13 +02:00
Bendik Aagaard LynghaugandClaude Fable 5.1 63179b9c47 fonts: fall back to fontconfig when a pinned font path is missing (0.4.2)
Release / package (x86_64, /var/local/cargo-target/iris-cargo-home, bare, /usr/bin/sccache, /var/local/rustup, /var/local/cargo-target) (push) Failing after 0s
Release / package (aarch64, , aarch64, , , ) (push) Failing after 1s
A config generated on one machine pinned
/usr/share/fonts/google-noto/NotoSans-Regular.ttf; on another host the
same package installs under fonts/noto, so the shared config made the
fontloader thread panic ("Font file not found") and, with panic =
"abort", kill wldash before a window appeared.

resolve_font_path() now uses a pinned path only if the file exists,
otherwise asks fontconfig for the name (what fc-match does) and says so
on stderr. find_font() and FontLoader::from_path() return errors
instead of unwrapping, and an unresolvable font exits with one clear
line. Generated configs leave font_paths unset so names resolve per
machine; pinning remains possible by hand.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 09:50:02 +00:00
Bendik Aagaard LynghaugandClaude Fable 5.1 d1df30d6d0 release 0.4.1
Release / package (x86_64, /var/local/cargo-target/iris-cargo-home, bare, /usr/bin/sccache, /var/local/rustup, /var/local/cargo-target) (push) Successful in 42s
Release / package (aarch64, , aarch64, , , ) (push) Successful in 2m14s
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 09:42:49 +00:00
Bendik Aagaard LynghaugandClaude Fable 5.1 9953865128 launcher: scan desktop files directly, keep state in XDG_STATE_HOME
The desktop-entry cache lived in $XDG_CACHE_HOME/wldash/desktop.json.
On an NFS-shared home that is a remote read and write on every start
(unless the session happens to export a machine-local XDG_CACHE_HOME),
and it went stale whenever a package was installed until someone ran
--desktop-refresh. Scanning the applications directories takes a few
milliseconds over local /usr/share and already happened on a background
thread, so the cache bought nothing: drop it, along with simd-json and
the serde derives on Desktop. --desktop-refresh stays as a no-op so old
key bindings keep working.

Calculator history and the prompt saved on Escape are state, not cache:
they move to $XDG_STATE_HOME/wldash (~/.local/state), written only on
Enter/Escape, and the old cache location is still read as a fallback.

With the serde derives gone, two Desktop fields were unused; use them:
Type=Link entries now open their URL with xdg-open, and Terminal=true
entries run under $TERMINAL -e when it is set.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 09:33:12 +00:00
13 changed files with 273 additions and 324 deletions
+17
View File
@@ -1,5 +1,22 @@
# Changelog # Changelog
## Unreleased
## 0.4.3 (2026-10-05)
- Backlight: a config with a `!backlight` widget on a machine without `/sys/class/backlight` entries (or with a `device` name that does not exist) panicked at start ("did not find backlight device"), fatal with `panic = "abort"`. The widget now warns once on stderr, renders empty and ignores clicks, so one config can be shared between a laptop and a desktop.
## 0.4.2 (2026-10-03)
- Fonts: a `font_paths` entry pointing at a file that does not exist on this machine made the font-loader thread panic and abort wldash ("Font file not found"). Pinned paths that are missing now fall back to fontconfig by name, with one line on stderr; a font that cannot be resolved at all exits with a clear message instead of a backtrace.
- `--write-default-config` no longer writes absolute font paths. Configs shared between machines with different font package layouts (`fonts/noto` vs `fonts/google-noto`) were breaking on the second host; names are resolved per machine instead. Existing configs keep working, and `font_paths` can still pin a file on purpose.
## 0.4.1 (2026-10-03)
- No more desktop-file cache: wldash scanned `~/.cache/wldash/desktop.json` (on NFS for shared homes, and stale after every package install) and needed a manual `--desktop-refresh`. Desktop files are now scanned on a background thread at every start, a few milliseconds over local `/usr/share`. `--desktop-refresh` is a no-op kept for key bindings; `simd-json` is gone.
- Calculator history and the Escape-saved prompt moved from `$XDG_CACHE_HOME/wldash` to `$XDG_STATE_HOME/wldash` (`~/.local/state`), where small roaming state belongs; old files are still read.
- Launcher: `Type=Link` entries open their URL with `xdg-open`; `Terminal=true` entries run inside `$TERMINAL -e` when that variable is set.
## 0.4.0 (2026-10-03) ## 0.4.0 (2026-10-03)
First release of the project.uhhm.no fork, packaged for the `[uhhm]` Arch registry. Numbering: upstream's old code base was tagged up to v0.3.0 and the rewrite called itself 0.2.0; this fork continues above both so pacman sees a monotonic version. First release of the project.uhhm.no fork, packaged for the `[uhhm]` Arch registry. Numbering: upstream's old code base was tagged up to v0.3.0 and the rewrite called itself 0.2.0; this fork continues above both so pacman sees a monotonic version.
Generated
+2 -217
View File
@@ -8,18 +8,6 @@ version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046"
[[package]]
name = "ahash"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77c3a9648d43b9cd48db467b3f87fdd6e146bcc88ab0180006cef2179fe11d01"
dependencies = [
"cfg-if",
"once_cell",
"version_check",
"zerocopy",
]
[[package]] [[package]]
name = "android-tzdata" name = "android-tzdata"
version = "0.1.1" version = "0.1.1"
@@ -188,15 +176,6 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "float-cmp"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
dependencies = [
"num-traits",
]
[[package]] [[package]]
name = "fontconfig" name = "fontconfig"
version = "0.8.0" version = "0.8.0"
@@ -225,38 +204,6 @@ dependencies = [
"version_check", "version_check",
] ]
[[package]]
name = "getrandom"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi",
"wasm-bindgen",
]
[[package]]
name = "halfbrown"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5681137554ddff44396e5f149892c769d45301dd9aa19c51602a89ee214cb0ec"
dependencies = [
"hashbrown 0.13.2",
"serde",
]
[[package]]
name = "hashbrown"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e"
dependencies = [
"ahash",
]
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.14.3" version = "0.14.3"
@@ -299,7 +246,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "433de089bd45971eecf4668ee0ee8f4cec17db4f8bd8f7bc3197a6ce37aa7d9b" checksum = "433de089bd45971eecf4668ee0ee8f4cec17db4f8bd8f7bc3197a6ce37aa7d9b"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.14.3", "hashbrown",
] ]
[[package]] [[package]]
@@ -334,70 +281,6 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "lexical-core"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cde5de06e8d4c2faabc400238f9ae1c74d5412d03a7bd067645ccbc47070e46"
dependencies = [
"lexical-parse-float",
"lexical-parse-integer",
"lexical-util",
"lexical-write-float",
"lexical-write-integer",
]
[[package]]
name = "lexical-parse-float"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683b3a5ebd0130b8fb52ba0bdc718cc56815b6a097e28ae5a6997d0ad17dc05f"
dependencies = [
"lexical-parse-integer",
"lexical-util",
"static_assertions",
]
[[package]]
name = "lexical-parse-integer"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d0994485ed0c312f6d965766754ea177d07f9c00c9b82a5ee62ed5b47945ee9"
dependencies = [
"lexical-util",
"static_assertions",
]
[[package]]
name = "lexical-util"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5255b9ff16ff898710eb9eb63cb39248ea8a5bb036bea8085b1a767ff6c4e3fc"
dependencies = [
"static_assertions",
]
[[package]]
name = "lexical-write-float"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accabaa1c4581f05a3923d1b4cfd124c329352288b7b9da09e766b0668116862"
dependencies = [
"lexical-util",
"lexical-write-integer",
"static_assertions",
]
[[package]]
name = "lexical-write-integer"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1b6f3d1f4422866b68192d62f77bc5c700bee84f3069f2469d7bc8c77852446"
dependencies = [
"lexical-util",
"static_assertions",
]
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.152" version = "0.2.152"
@@ -693,26 +576,6 @@ dependencies = [
"pest_derive", "pest_derive",
] ]
[[package]]
name = "ref-cast"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4846d4c50d1721b1a3bef8af76924eef20d5e723647333798c1b519b3a9473f"
dependencies = [
"ref-cast-impl",
]
[[package]]
name = "ref-cast-impl"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fddb4f8d99b0a2ebafc65a87a69a7b9875e4b1ae1f00db265d300ef7f28bccc"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
]
[[package]] [[package]]
name = "rusttype" name = "rusttype"
version = "0.9.3" version = "0.9.3"
@@ -764,17 +627,6 @@ dependencies = [
"syn 2.0.48", "syn 2.0.48",
] ]
[[package]]
name = "serde_json"
version = "1.0.113"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]] [[package]]
name = "serde_yaml" name = "serde_yaml"
version = "0.9.31" version = "0.9.31"
@@ -799,28 +651,6 @@ dependencies = [
"digest", "digest",
] ]
[[package]]
name = "simd-json"
version = "0.13.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2faf8f101b9bc484337a6a6b0409cf76c139f2fb70a9e3aee6b6774be7bfbf76"
dependencies = [
"getrandom",
"halfbrown",
"lexical-core",
"ref-cast",
"serde",
"serde_json",
"simdutf8",
"value-trait",
]
[[package]]
name = "simdutf8"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a"
[[package]] [[package]]
name = "slotmap" name = "slotmap"
version = "1.0.7" version = "1.0.7"
@@ -836,12 +666,6 @@ version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7"
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.109" version = "1.0.109"
@@ -930,18 +754,6 @@ version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab4c90930b95a82d00dc9e9ac071b4991924390d46cbd0dfe566148667605e4b" checksum = "ab4c90930b95a82d00dc9e9ac071b4991924390d46cbd0dfe566148667605e4b"
[[package]]
name = "value-trait"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dad8db98c1e677797df21ba03fca7d3bf9bec3ca38db930954e4fe6e1ea27eb4"
dependencies = [
"float-cmp",
"halfbrown",
"itoa",
"ryu",
]
[[package]] [[package]]
name = "vec_map" name = "vec_map"
version = "0.8.2" version = "0.8.2"
@@ -964,12 +776,6 @@ dependencies = [
"winapi-util", "winapi-util",
] ]
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.90" version = "0.2.90"
@@ -1265,7 +1071,7 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
[[package]] [[package]]
name = "wldash" name = "wldash"
version = "0.4.0" version = "0.4.3"
dependencies = [ dependencies = [
"calloop", "calloop",
"chrono", "chrono",
@@ -1279,7 +1085,6 @@ dependencies = [
"rusttype", "rusttype",
"serde", "serde",
"serde_yaml", "serde_yaml",
"simd-json",
"unicode-segmentation", "unicode-segmentation",
"walkdir", "walkdir",
"wayland-client", "wayland-client",
@@ -1316,23 +1121,3 @@ dependencies = [
"once_cell", "once_cell",
"pkg-config", "pkg-config",
] ]
[[package]]
name = "zerocopy"
version = "0.7.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
]
+1 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "wldash" name = "wldash"
version = "0.4.0" version = "0.4.3"
edition = "2024" edition = "2024"
[profile.release] [profile.release]
@@ -38,5 +38,4 @@ libpulse-binding = "2.28"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_yaml = "0.9" serde_yaml = "0.9"
simd-json = "0.13"
+1 -1
View File
@@ -4,7 +4,7 @@
# project.uhhm.no (namespace bl). pkgver is the version; the tag has # project.uhhm.no (namespace bl). pkgver is the version; the tag has
# to agree with it and with Cargo.toml. # to agree with it and with Cargo.toml.
pkgname=wldash pkgname=wldash
pkgver=0.4.0 pkgver=0.4.3
pkgrel=1 pkgrel=1
pkgdesc="Dashboard/launcher/control-panel for wlroots-style Wayland compositors (layer-shell)" pkgdesc="Dashboard/launcher/control-panel for wlroots-style Wayland compositors (layer-shell)"
arch=('x86_64' 'aarch64') arch=('x86_64' 'aarch64')
+14 -1
View File
@@ -28,7 +28,9 @@ per-host layouts apart on NFS-shared homes.
To generate a new default configuration, run `wldash --write-default-config`. To configure, edit `$XDG_CONFIG_HOME/wldash/config.yml` (or if `XDG_CONFIG_HOME` is not set, `~/.config/wldash/config.yml`). If `/etc/wldash/config.yml` exists it takes precedence over the per-user file; `--config FILE` overrides both. To generate a new default configuration, run `wldash --write-default-config`. To configure, edit `$XDG_CONFIG_HOME/wldash/config.yml` (or if `XDG_CONFIG_HOME` is not set, `~/.config/wldash/config.yml`). If `/etc/wldash/config.yml` exists it takes precedence over the per-user file; `--config FILE` overrides both.
To regenerate the desktop file cache, run `wldash --desktop-refresh`. Fonts are named (`sans`, `monospace`, or any fontconfig name) and resolved through fontconfig on the machine wldash runs on. `font_paths` in the config can pin a name to a file; if that file does not exist on the current machine, wldash says so on stderr and falls back to fontconfig rather than failing, so one config can roam between hosts with different font package layouts.
Desktop files are scanned every time wldash starts (on a background thread), so there is no cache to refresh; `--desktop-refresh` is kept as a no-op for old key bindings. Calculator history and the prompt saved on Escape live in `$XDG_STATE_HOME/wldash` (`~/.local/state/wldash`); older installs' files under `~/.cache/wldash` are still read.
For more info, look in `src/config.rs`. For more info, look in `src/config.rs`.
@@ -77,6 +79,17 @@ The launcher also accepts prefix operators to change its mode:
- Scaling of any kind - all sizes are currently hardcoded in pixels - Scaling of any kind - all sizes are currently hardcoded in pixels
## Releasing
Releases are cut with [cargo-release](https://github.com/crate-ci/cargo-release) (`cargo install cargo-release`), configured in `release.toml`. Put a line for each change under `## Unreleased` in `CHANGELOG.md` as you go, then on a clean `master`:
```
cargo release patch # dry run: shows the bump, replacements and tag
cargo release patch --execute # do it (minor/major or an explicit version also work)
```
This bumps `Cargo.toml`, `Cargo.lock` and `PKGBUILD` to the same version, stamps the changelog section with the version and date, commits as `release X.Y.Z`, tags `vX.Y.Z` and pushes commit and tag to the `gitea` remote. The tag triggers `.gitea/workflows/release.yml`, which builds the package per architecture and uploads it to the `[uhhm]` registry. The workflow refuses a tag whose version disagrees with `PKGBUILD` or `Cargo.toml`, which cannot happen when the tag comes from cargo-release.
## How to discuss ## How to discuss
Go to #kennylevinsen @ irc.libera.chat to discuss, or use [~kennylevinsen/public-inbox@lists.sr.ht](https://lists.sr.ht/~kennylevinsen/public-inbox). Go to #kennylevinsen @ irc.libera.chat to discuss, or use [~kennylevinsen/public-inbox@lists.sr.ht](https://lists.sr.ht/~kennylevinsen/public-inbox).
+26
View File
@@ -0,0 +1,26 @@
# cargo-release settings. Cut a release with
#
# cargo release patch # dry run, shows what would change
# cargo release patch --execute
#
# (or `minor`, `major`, or an explicit version). It bumps Cargo.toml and
# Cargo.lock, rewrites PKGBUILD's pkgver, turns the "## Unreleased"
# section of CHANGELOG.md into "## X.Y.Z (date)", commits, tags
# vX.Y.Z and pushes commit + tag to the gitea remote. That tag is what
# .gitea/workflows/release.yml builds and uploads to the [uhhm] registry.
# Not on crates.io; the package goes to the Arch registry via the tag.
publish = false
# `origin` is the upstream kennylevinsen fork on GitHub; releases go to
# project.uhhm.no, and the workflow only runs there.
push-remote = "gitea"
allow-branch = ["master"]
tag-name = "v{{version}}"
tag-message = "wldash {{version}}"
pre-release-commit-message = "release {{version}}"
pre-release-replacements = [
{ file = "PKGBUILD", search = "^pkgver=.*", replace = "pkgver={{version}}", exactly = 1 },
{ file = "PKGBUILD", search = "^pkgrel=.*", replace = "pkgrel=1", exactly = 1 },
{ file = "CHANGELOG.md", search = "^## Unreleased$", replace = "## {{version}} ({{date}})", exactly = 1 },
]
+11 -17
View File
@@ -6,7 +6,7 @@ use std::sync::{Arc, Mutex};
use crate::{ use crate::{
event::Events, event::Events,
fonts::{FontMap, find_font}, fonts::FontMap,
widgets::{ widgets::{
Backlight, Battery, Calendar, Clock, Date, HorizontalLayout, IndexedLayout, Interface, Backlight, Battery, Calendar, Clock, Date, HorizontalLayout, IndexedLayout, Interface,
InvertedHorizontalLayout, InvertedVerticalLayout, Layout, Line, Margin, PulseAudio, InvertedHorizontalLayout, InvertedVerticalLayout, Layout, Line, Margin, PulseAudio,
@@ -89,8 +89,6 @@ impl Config {
pub fn generate_v1() -> Config { pub fn generate_v1() -> Config {
let has_battery = Battery::detect(); let has_battery = Battery::detect();
let has_backlight = Backlight::detect(); let has_backlight = Backlight::detect();
let sans = find_font("sans");
let monospace = find_font("monospace");
let mut bars = Vec::new(); let mut bars = Vec::new();
if has_battery { if has_battery {
@@ -154,13 +152,12 @@ impl Config {
])), ])),
}; };
let map = HashMap::from([
("sans".to_string(), sans),
("monospace".to_string(), monospace),
]);
Config { Config {
font_paths: Some(map), // Left unset on purpose: "sans"/"monospace" are resolved through
// fontconfig on each machine at start. Pinning absolute paths
// here broke configs shared between hosts with different font
// package layouts. Set font_paths yourself to override.
font_paths: None,
widget, widget,
mode: OperationMode::LayerSurface((1024, 768)), mode: OperationMode::LayerSurface((1024, 768)),
background: Some(0xFAFAFAFA), background: Some(0xFAFAFAFA),
@@ -171,8 +168,6 @@ impl Config {
pub fn generate_v2(clay: bool) -> Config { pub fn generate_v2(clay: bool) -> Config {
let has_battery = Battery::detect(); let has_battery = Battery::detect();
let has_backlight = Backlight::detect(); let has_backlight = Backlight::detect();
let sans = find_font("sans");
let monospace = find_font("monospace");
let mut bars = Vec::new(); let mut bars = Vec::new();
if has_battery { if has_battery {
@@ -244,13 +239,12 @@ impl Config {
]), ]),
]); ]);
let map = HashMap::from([
("sans".to_string(), sans),
("monospace".to_string(), monospace),
]);
Config { Config {
font_paths: Some(map), // Left unset on purpose: "sans"/"monospace" are resolved through
// fontconfig on each machine at start. Pinning absolute paths
// here broke configs shared between hosts with different font
// package layouts. Set font_paths yourself to override.
font_paths: None,
widget, widget,
mode: if clay { mode: if clay {
OperationMode::XdgToplevel OperationMode::XdgToplevel
+84 -21
View File
@@ -9,21 +9,48 @@ use std::{
/// FontRef is used to store Fonts on widgets. /// FontRef is used to store Fonts on widgets.
pub type FontRef<'a> = &'a rusttype::Font<'a>; pub type FontRef<'a> = &'a rusttype::Font<'a>;
/// Ask fontconfig for the file backing a font name ("sans", "monospace",
/// "Noto Sans", ...), the way fc-match does.
#[cfg(feature = "fontconfig")] #[cfg(feature = "fontconfig")]
pub(crate) fn find_font(name: &str) -> String { pub(crate) fn find_font(name: &str) -> Result<String, String> {
use fontconfig::Fontconfig as FontConfig; use fontconfig::Fontconfig as FontConfig;
let fc = FontConfig::new().unwrap(); let fc = FontConfig::new().ok_or("could not initialise fontconfig")?;
fc.find(name, None) let found = fc
.unwrap() .find(name, None)
.ok_or_else(|| format!("fontconfig has no match for {:?}", name))?;
found
.path .path
.to_str() .to_str()
.unwrap() .map(|s| s.to_string())
.to_string() .ok_or_else(|| format!("font path for {:?} is not UTF-8", name))
} }
#[cfg(not(feature = "fontconfig"))] #[cfg(not(feature = "fontconfig"))]
pub(crate) fn find_font(_name: &str) -> String { pub(crate) fn find_font(name: &str) -> Result<String, String> {
panic!("fontconfig not enabled so font search not available"); Err(format!(
"no path configured for font {:?} and wldash was built without fontconfig",
name
))
}
/// Resolve the file to load for `name`: a path pinned in the config if it
/// exists on this machine, otherwise whatever fontconfig picks for the name.
///
/// Configs roam between machines on a shared home, and font packages do
/// not keep the same file layout everywhere (Noto under fonts/noto on one
/// distribution, fonts/google-noto on another), so a pinned path that is
/// missing is a reason to fall back, not to abort.
pub(crate) fn resolve_font_path(name: &str, pinned: Option<&str>) -> Result<String, String> {
if let Some(p) = pinned {
if Path::new(p).is_file() {
return Ok(p.to_string());
}
eprintln!(
"wldash: configured font file for {:?} not found: {}; asking fontconfig instead",
name, p
);
}
find_font(name)
} }
/// FontLoader is a marker struct that is used to load files /// FontLoader is a marker struct that is used to load files
@@ -31,17 +58,20 @@ pub(crate) struct FontLoader;
impl FontLoader { impl FontLoader {
/// Given a path, loads it as a Font, which can be rendered to the screen. /// Given a path, loads it as a Font, which can be rendered to the screen.
pub(crate) fn from_path<'a, P>(path: P) -> Option<Font<'a>> pub(crate) fn from_path<'a, P>(path: P) -> Result<Font<'a>, String>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
let mut file = File::open(path).expect("Font file not found"); let path = path.as_ref();
let mut file =
File::open(path).map_err(|e| format!("cannot open {}: {}", path.display(), e))?;
let mut data = match file.metadata() { let mut data = match file.metadata() {
Ok(metadata) => Vec::with_capacity(metadata.len() as usize), Ok(metadata) => Vec::with_capacity(metadata.len() as usize),
Err(_) => vec![], Err(_) => vec![],
}; };
file.read_to_end(&mut data).unwrap(); file.read_to_end(&mut data)
Font::try_from_vec(data) .map_err(|e| format!("cannot read {}: {}", path.display(), e))?;
Font::try_from_vec(data).ok_or_else(|| format!("{} is not a usable font", path.display()))
} }
} }
@@ -102,19 +132,28 @@ impl FontMap {
self.font_paths.insert(font_name, font_path); self.font_paths.insert(font_name, font_path);
} }
/// Load every queued font. Runs on its own thread; a font that cannot
/// be found or read is a configuration problem the user has to see, so
/// it is reported in one line and the process exits instead of
/// aborting with a panic backtrace.
pub fn load_fonts(&mut self) { pub fn load_fonts(&mut self) {
for (font_name, v) in self.required_fonts.iter() { for (font_name, v) in self.required_fonts.iter() {
let path = match self.font_paths.get(font_name) { let pinned = self.font_paths.get(font_name).map(|s| s.as_str());
Some(res) => res, let path = match resolve_font_path(font_name, pinned) {
_ => { Ok(p) => p,
let s = find_font(font_name); Err(e) => {
self.font_paths.insert(font_name, s); eprintln!("wldash: cannot resolve font {:?}: {}", font_name, e);
self.font_paths.get(font_name).unwrap() std::process::exit(1);
} }
}; };
let fontref = Box::leak(Box::new( let font = match FontLoader::from_path(&path) {
FontLoader::from_path(path).expect("unable to load font"), Ok(f) => f,
)); Err(e) => {
eprintln!("wldash: cannot load font {:?}: {}", font_name, e);
std::process::exit(1);
}
};
let fontref = Box::leak(Box::new(font));
for (size, preload) in &v.1 { for (size, preload) in &v.1 {
if let Some(font) = self.fonts.get_mut(&(font_name, ComparableF32(*size))) { if let Some(font) = self.fonts.get_mut(&(font_name, ComparableF32(*size))) {
font.add_str_to_cache(preload); font.add_str_to_cache(preload);
@@ -160,3 +199,27 @@ impl MaybeFontMap {
} }
} }
} }
#[cfg(test)]
mod tests {
use super::resolve_font_path;
#[test]
fn pinned_path_wins_when_present() {
let dir = std::env::temp_dir().join(format!("wldash-font-test-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let f = dir.join("x.ttf");
std::fs::write(&f, b"not really a font").unwrap();
let p = f.to_str().unwrap();
assert_eq!(resolve_font_path("sans", Some(p)).unwrap(), p);
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn missing_pinned_path_falls_back_to_lookup() {
// Whatever fontconfig answers (or an error without fontconfig),
// the missing pinned path itself must never be returned.
let r = resolve_font_path("sans", Some("/nonexistent/wldash/font.ttf"));
assert_ne!(r.as_deref().ok(), Some("/nonexistent/wldash/font.ttf"));
}
}
+4 -11
View File
@@ -23,10 +23,7 @@ use event::{Event, Events};
use fonts::{FontMap, MaybeFontMap}; use fonts::{FontMap, MaybeFontMap};
use keyboard::{KeyRepeatSource, RepeatMessage}; use keyboard::{KeyRepeatSource, RepeatMessage};
use state::State; use state::State;
use utils::{ use utils::xdg;
desktop::{load_desktop_files, write_desktop_cache},
xdg,
};
use widgets::{Geometry, Widget}; use widgets::{Geometry, Widget};
use std::{ use std::{
@@ -50,7 +47,8 @@ OPTIONS:
else ~/.config/wldash/config.yml) else ~/.config/wldash/config.yml)
--write-default-config [v1|v2|toplevel] generate and write a new config file --write-default-config [v1|v2|toplevel] generate and write a new config file
(v2 by default) (v2 by default)
--desktop-refresh refresh desktop file cache --desktop-refresh no-op kept for old key bindings
(desktop files are scanned at start)
" "
); );
} }
@@ -68,9 +66,7 @@ fn ensure_dir_exists(path: &str) {
fn create_required_directories() { fn create_required_directories() {
let dir_config_wldash = format!("{}/wldash", xdg::config_folder()); let dir_config_wldash = format!("{}/wldash", xdg::config_folder());
let dir_cache_wldash = format!("{}/wldash", xdg::cache_folder());
ensure_dir_exists(&dir_config_wldash); ensure_dir_exists(&dir_config_wldash);
ensure_dir_exists(&dir_cache_wldash);
} }
/// Default config path. On a shared (e.g. NFS-roamed) home, a host-specific /// Default config path. On a shared (e.g. NFS-roamed) home, a host-specific
@@ -119,10 +115,7 @@ fn main() {
std::process::exit(0); std::process::exit(0);
} }
Some(ref s) if s == "--desktop-refresh" => { Some(ref s) if s == "--desktop-refresh" => {
let v = load_desktop_files(); eprintln!("wldash: desktop files are scanned at every start; nothing to refresh");
if let Err(e) = write_desktop_cache(&v) {
die(format!("could not write desktop cache: {}", e));
}
std::process::exit(0); std::process::exit(0);
} }
Some(ref s) if s == "--help" => { Some(ref s) if s == "--help" => {
+9 -19
View File
@@ -2,20 +2,17 @@ use std::{
cmp::Ordering, cmp::Ordering,
collections::HashSet, collections::HashSet,
error::Error, error::Error,
fs::{File, read_to_string}, fs::read_to_string,
hash::{Hash, Hasher}, hash::{Hash, Hasher},
io::{Error as io_error, ErrorKind}, io::{Error as io_error, ErrorKind},
path::Path, path::Path,
}; };
use serde::{self, Deserialize, Serialize};
use simd_json;
use walkdir::WalkDir; use walkdir::WalkDir;
use crate::utils::{inish, xdg}; use crate::utils::{inish, xdg};
#[derive(Serialize, Deserialize, Clone, Debug, Eq)] #[derive(Clone, Debug, Eq)]
#[serde(rename_all = "snake_case")]
pub struct Desktop { pub struct Desktop {
pub entry_type: String, pub entry_type: String,
pub name: String, pub name: String,
@@ -83,20 +80,13 @@ impl Hash for Desktop {
} }
} }
pub fn load_desktop_cache() -> Result<Vec<Desktop>, Box<dyn Error>> { /// Scan every XDG applications directory for launchable entries.
match File::open(format!("{}/wldash/desktop.json", xdg::cache_folder())) { ///
Ok(f) => simd_json::from_reader(f).map_err(|e| e.into()), /// This used to be cached as JSON under XDG_CACHE_HOME and refreshed by
Err(e) => Err(e.into()), /// hand with `--desktop-refresh`. The scan is a few milliseconds over
} /// local /usr/share and runs on a background thread, while the cache sat
} /// on the (possibly NFS-mounted) home directory and went stale whenever a
/// package was installed. So: no cache.
pub fn write_desktop_cache(v: &Vec<Desktop>) -> Result<(), Box<dyn Error>> {
match File::create(format!("{}/wldash/desktop.json", xdg::cache_folder())) {
Ok(f) => simd_json::to_writer(f, v).map_err(|e| e.into()),
Err(e) => Err(e.into()),
}
}
pub fn load_desktop_files() -> Vec<Desktop> { pub fn load_desktop_files() -> Vec<Desktop> {
let dirs = xdg::data_folders(); let dirs = xdg::data_folders();
+12
View File
@@ -10,6 +10,18 @@ pub fn cache_folder() -> String {
} }
} }
/// XDG_STATE_HOME: small persistent state (calculator history, saved
/// prompt). Unlike a cache this is worth roaming with the home directory.
pub fn state_folder() -> String {
match env::var_os("XDG_STATE_HOME") {
Some(s) => s.into_string().unwrap(),
None => {
let home = env::var_os("HOME").unwrap().into_string().unwrap();
format!("{}/.local/state", home)
}
}
}
pub fn config_folder() -> String { pub fn config_folder() -> String {
match env::var_os("XDG_CONFIG_HOME") { match env::var_os("XDG_CONFIG_HOME") {
Some(s) => s.into_string().unwrap(), Some(s) => s.into_string().unwrap(),
+43 -11
View File
@@ -26,7 +26,11 @@ fn write_file_as_u64(path: &Path, value: u64) -> Result<(), Error> {
} }
pub struct Backlight { pub struct Backlight {
device_path: PathBuf, /// `None` when no backlight exists on this machine: the widget then
/// renders empty and ignores clicks instead of taking wldash down.
/// Configs are shared between hosts, so a laptop config running on a
/// desktop must not panic here.
device_path: Option<PathBuf>,
cur: u64, cur: u64,
max: u64, max: u64,
dirty: bool, dirty: bool,
@@ -46,10 +50,13 @@ fn find_device() -> Option<PathBuf> {
impl Backlight { impl Backlight {
fn update(&mut self) { fn update(&mut self) {
if let Ok(cur) = read_file_as_u64(self.device_path.join("brightness").as_path()) { let Some(dev) = &self.device_path else {
return;
};
if let Ok(cur) = read_file_as_u64(dev.join("brightness").as_path()) {
self.cur = cur; self.cur = cur;
} }
if let Ok(max) = read_file_as_u64(self.device_path.join("max_brightness").as_path()) { if let Ok(max) = read_file_as_u64(dev.join("max_brightness").as_path()) {
self.max = max; self.max = max;
} }
} }
@@ -58,11 +65,30 @@ impl Backlight {
#[allow(clippy::new_ret_no_self)] #[allow(clippy::new_ret_no_self)]
pub fn new(path: Option<&str>, fm: &mut FontMap, font: &'static str, size: f32) -> BarWidget { pub fn new(path: Option<&str>, fm: &mut FontMap, font: &'static str, size: f32) -> BarWidget {
let device_path = match path { let device_path = match path {
Some(path) => Some(Path::new("/sys/class/backlight").join(path)), Some(path) => {
None => find_device(), let p = Path::new("/sys/class/backlight").join(path);
if p.is_dir() {
Some(p)
} else {
eprintln!(
"wldash: backlight device {:?} not found, widget disabled",
p
);
None
}
}
None => {
let p = find_device();
if p.is_none() {
eprintln!(
"wldash: no backlight device under /sys/class/backlight, widget disabled"
);
}
p
}
}; };
let mut dev = Backlight { let mut dev = Backlight {
device_path: device_path.expect("did not find backlight device"), device_path,
cur: 0, cur: 0,
max: 0, max: 0,
dirty: true, dirty: true,
@@ -77,9 +103,13 @@ impl Backlight {
} }
pub fn set(&mut self, brightness: f32) { pub fn set(&mut self, brightness: f32) {
let Some(dev) = &self.device_path else {
return;
};
let val = (self.max as f32 * brightness.clamp(0.0, 1.0)) as u64; let val = (self.max as f32 * brightness.clamp(0.0, 1.0)) as u64;
let _ = write_file_as_u64(self.device_path.join("brightness").as_path(), val); let brightness_path = dev.join("brightness");
if let Ok(cur) = read_file_as_u64(self.device_path.join("brightness").as_path()) { let _ = write_file_as_u64(&brightness_path, val);
if let Ok(cur) = read_file_as_u64(&brightness_path) {
self.cur = cur; self.cur = cur;
} }
} }
@@ -93,12 +123,14 @@ impl BarWidgetImpl for Backlight {
self.dirty self.dirty
} }
fn value(&mut self) -> f32 { fn value(&mut self) -> f32 {
self.dirty = false;
if self.device_path.is_none() {
return 0.0;
}
if self.max == 0 || self.cur > self.max { if self.max == 0 || self.cur > self.max {
// what. // Sysfs handed us nonsense; show full rather than a bogus ratio.
return 1.0; return 1.0;
} }
self.dirty = false;
self.cur as f32 / self.max as f32 self.cur as f32 / self.max as f32
} }
fn color(&self) -> Color { fn color(&self) -> Color {
+49 -24
View File
@@ -16,7 +16,7 @@ use crate::{
fonts::FontMap, fonts::FontMap,
keyboard::{KeyEvent, keysyms}, keyboard::{KeyEvent, keysyms},
utils::{ utils::{
desktop::{Desktop, load_desktop_cache, load_desktop_files, write_desktop_cache}, desktop::{Desktop, load_desktop_files},
xdg, xdg,
}, },
widgets::{Geometry, Widget}, widgets::{Geometry, Widget},
@@ -225,6 +225,29 @@ fn strip_field_codes(exec: &str) -> String {
out out
} }
/// Path of a small persistent state file (calc history, saved prompt)
/// under $XDG_STATE_HOME/wldash.
fn state_path(name: &str) -> String {
format!("{}/wldash/{}", xdg::state_folder(), name)
}
/// Read a state file, falling back to where older versions kept it
/// ($XDG_CACHE_HOME/wldash) so nothing is lost on upgrade.
fn read_state(name: &str) -> Option<String> {
read_to_string(state_path(name))
.or_else(|_| read_to_string(format!("{}/wldash/{}", xdg::cache_folder(), name)))
.ok()
}
/// Create (truncate) a state file, making the directory on demand.
fn create_state(name: &str) -> Option<File> {
let path = state_path(name);
if let Some(dir) = std::path::Path::new(&path).parent() {
let _ = std::fs::create_dir_all(dir);
}
File::create(path).ok()
}
trait InterfaceWidget { trait InterfaceWidget {
/// Number of selectable rows (used to clamp the selection). /// Number of selectable rows (used to clamp the selection).
fn rows(&self) -> usize; fn rows(&self) -> usize;
@@ -253,16 +276,8 @@ impl Launcher {
thread::Builder::new() thread::Builder::new()
.name("desktopini".to_string()) .name("desktopini".to_string())
.spawn(move || { .spawn(move || {
let mut options = options.lock().unwrap(); let loaded = load_desktop_files();
*options = match load_desktop_cache() { *options.lock().unwrap() = loaded;
Ok(v) => v,
Err(_) => {
let v = load_desktop_files();
write_desktop_cache(&v).unwrap();
v
}
};
drop(options);
let mut events = events.lock().unwrap(); let mut events = events.lock().unwrap();
events.add_event(Event::LauncherUpdate); events.add_event(Event::LauncherUpdate);
}) })
@@ -306,9 +321,19 @@ impl InterfaceWidget for Launcher {
fn trigger(&mut self, intf: &mut InnerInterface) { fn trigger(&mut self, intf: &mut InnerInterface) {
if self.matches.len() > intf.selection { if self.matches.len() > intf.selection {
let d = &self.matches[intf.selection]; let d = &self.matches[intf.selection];
if let Some(exec) = &d.exec { let cmd = match (&d.exec, &d.url) {
self.exec(&strip_field_codes(exec)); (Some(exec), _) => strip_field_codes(exec),
} // Type=Link entries have no Exec; open the URL instead.
(None, Some(url)) => format!("xdg-open '{}'", url.replace('\'', "'\\''")),
(None, None) => return,
};
// Terminal=true: run inside the user's terminal when one is
// configured, otherwise as before.
let cmd = match (d.term, std::env::var("TERMINAL")) {
(true, Ok(term)) if !term.is_empty() => format!("{} -e {}", term, cmd),
_ => cmd,
};
self.exec(&cmd);
} }
} }
@@ -502,9 +527,9 @@ impl Calc {
thread::Builder::new() thread::Builder::new()
.name("calc".to_string()) .name("calc".to_string())
.spawn(move || { .spawn(move || {
let s = match read_to_string(format!("{}/wldash/calc", xdg::cache_folder())) { let s = match read_state("calc") {
Ok(s) => s, Some(s) => s,
_ => return, None => return,
}; };
let mut old = old_local.lock().unwrap(); let mut old = old_local.lock().unwrap();
@@ -524,9 +549,9 @@ impl Calc {
} }
fn sync(&self) { fn sync(&self) {
let mut f = match File::create(format!("{}/wldash/calc", xdg::cache_folder())) { let mut f = match create_state("calc") {
Ok(f) => f, Some(f) => f,
_ => return, None => return,
}; };
let old = self.old.lock().unwrap(); let old = self.old.lock().unwrap();
@@ -713,9 +738,9 @@ impl Interface {
std::process::exit(0); std::process::exit(0);
} }
if let Ok(mut f) = File::create(format!("{}/wldash/prompt", xdg::cache_folder())) { if let Some(mut f) = create_state("prompt") {
write!(f, "{}", self.inner.prompt.input).unwrap(); let _ = write!(f, "{}", self.inner.prompt.input);
f.sync_data().unwrap(); let _ = f.sync_data();
} }
std::process::exit(0); std::process::exit(0);
} }
@@ -744,7 +769,7 @@ impl Interface {
widget.update(&self.inner); widget.update(&self.inner);
} }
keysyms::KEY_r if event.modifiers.ctrl => { keysyms::KEY_r if event.modifiers.ctrl => {
if let Ok(s) = read_to_string(format!("{}/wldash/prompt", xdg::cache_folder())) { if let Some(s) = read_state("prompt") {
self.inner.prompt.set(&s); self.inner.prompt.set(&s);
} }
} }