- Linux: Tauri packs a window's webviews into one GTK box with equal shares and ignores their bounds, so the title bar took half the window. The title bar webview now has a fixed height and no share; the box's border is the resize frame (none when maximized). - tauri-plugin-updater: checks llembas.eu/desktop/latest.json at start and every 6 h, an update button in the title bar and menu entries, .deb via pkexec, AppImage in place, then restart. Packages signed with the updater's minisign key; requireSignedVersion on. scripts/publish-update.sh signs a GitHub release and writes the manifest. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
10 files changed
+575
-15
No files matched your search
@@ -6,6 +6,18 @@ All notable changes to LLeMbas Desktop. The format follows
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Updates: the app looks for a new release at start and every six hours, shows an **update**
|
||||
button in the title bar when there is one, and installs it (the .deb with the system's password
|
||||
prompt, an AppImage in place) after checking its signature. The menu can also check by hand.
|
||||
|
||||
### Fixed
|
||||
|
||||
- On Linux the server's page took only the lower half of the window, under an empty title bar
|
||||
area. The title bar is now its fixed height and the page takes the rest; the frame you resize
|
||||
the window by is drawn around both.
|
||||
|
||||
## [0.2.0] — 2026-10-14
|
||||
|
||||
### Added
|
||||
|
||||
@@ -27,6 +27,14 @@ chmod +x llembas-desktop_*.AppImage && ./llembas-desktop_*.AppImage
|
||||
On GNOME the tray needs the *AppIndicator* extension; without it, the title bar's menu has
|
||||
everything, and starting the app again brings a hidden window back.
|
||||
|
||||
## Updates
|
||||
|
||||
LLeMbas Desktop looks for a new release when it starts and every six hours. When there is one, an
|
||||
**update** button appears in the title bar (and *Install LLeMbas x.y.z…* in its menu; *Check for
|
||||
updates…* otherwise). Installing asks for your password to update the `.deb` (an AppImage updates
|
||||
itself), then the app starts again. Every package is checked against the release signing key
|
||||
built into the app before anything is installed.
|
||||
|
||||
## Use
|
||||
|
||||
1. Start LLeMbas and enter your server's address. Sign in there as you would in a browser.
|
||||
|
||||
Executable
+47
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env bash
|
||||
# Offer a release to installed copies of LLeMbas Desktop: sign its packages with the updater key
|
||||
# and write the update manifest the app reads (https://llembas.eu/desktop/latest.json).
|
||||
#
|
||||
# MINISIGN_KEY=path/to/updater.key scripts/publish-update.sh v0.3.0 path/to/LLeMbas-Website
|
||||
#
|
||||
# The packages are the ones release.yml published on GitHub for the tag, checked against that
|
||||
# release's SHA256SUMS first. The app verifies each package's signature with the public key in
|
||||
# src-tauri/tauri.conf.json before installing it, so the manifest's host need not be trusted.
|
||||
# This writes site/desktop/latest.json in the website's checkout; committing and pushing it is
|
||||
# what publishes the update.
|
||||
set -euo pipefail
|
||||
TAG="${1:?usage: publish-update.sh vX.Y.Z path/to/LLeMbas-Website}"
|
||||
SITE="${2:?usage: publish-update.sh vX.Y.Z path/to/LLeMbas-Website}"
|
||||
KEY="${MINISIGN_KEY:?set MINISIGN_KEY to the minisign secret key of the updater}"
|
||||
VERSION="${TAG#v}"
|
||||
BASE="https://github.com/LLeMbas/LLeMbas-Desktop/releases/download/$TAG"
|
||||
DEB="llembas-desktop_${VERSION}_amd64.deb"
|
||||
APPIMAGE="llembas-desktop_${VERSION}_amd64.AppImage"
|
||||
|
||||
work="$(mktemp -d)"
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
for f in SHA256SUMS "$DEB" "$APPIMAGE"; do
|
||||
curl -fsSL --retry 3 -o "$work/$f" "$BASE/$f"
|
||||
done
|
||||
[ "$(sed -n -E 's/^# llembas-desktop //p' "$work/SHA256SUMS" | head -n1)" = "$VERSION" ] \
|
||||
|| { echo "publish-update: SHA256SUMS is not for $VERSION" >&2; exit 1; }
|
||||
(cd "$work" && grep -E " ($DEB|$APPIMAGE)\$" SHA256SUMS | sha256sum -c --strict --quiet -) \
|
||||
|| { echo "publish-update: the packages do not match SHA256SUMS" >&2; exit 1; }
|
||||
|
||||
sig() {
|
||||
# Tab-separated, with the version: the app refuses a package whose signature names another one.
|
||||
minisign -S -s "$KEY" -m "$work/$1" -x "$work/$1.minisig" \
|
||||
-t "$(printf 'timestamp:%s\tfile:%s\tversion:%s' "$(date +%s)" "$1" "$VERSION")" >/dev/null
|
||||
base64 -w0 "$work/$1.minisig"
|
||||
}
|
||||
notes="$(git tag -l --format='%(contents)' "$TAG" | sed '/-----BEGIN SSH SIGNATURE-----/,$d' | tail -n +3)"
|
||||
|
||||
mkdir -p "$SITE/site/desktop"
|
||||
jq -n --arg v "$VERSION" --arg notes "$notes" --arg date "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
||||
--arg deb_url "$BASE/$DEB" --arg deb_sig "$(sig "$DEB")" \
|
||||
--arg app_url "$BASE/$APPIMAGE" --arg app_sig "$(sig "$APPIMAGE")" \
|
||||
'{version: $v, notes: $notes, pub_date: $date, platforms: {
|
||||
"linux-x86_64-deb": {url: $deb_url, signature: $deb_sig},
|
||||
"linux-x86_64-appimage": {url: $app_url, signature: $app_sig}}}' \
|
||||
> "$SITE/site/desktop/latest.json"
|
||||
echo "publish-update: wrote $SITE/site/desktop/latest.json for $VERSION — commit and push it to publish"
|
||||
Generated
+317
-9
@@ -47,6 +47,15 @@ version = "1.0.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
||||
|
||||
[[package]]
|
||||
name = "arbitrary"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3bc62ac97cc33321f50863d514c3bc38a453947a8f9e781137e47c7401020aed"
|
||||
dependencies = [
|
||||
"derive_arbitrary",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-broadcast"
|
||||
version = "0.7.2"
|
||||
@@ -708,6 +717,17 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_arbitrary"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b034bd7d5f032402a2479444dcc6f74e36a03f31854d41680fb240ef682a1ac"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "2.1.1"
|
||||
@@ -976,6 +996,16 @@ dependencies = [
|
||||
"rustc_version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.14"
|
||||
@@ -1520,6 +1550,21 @@ dependencies = [
|
||||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-rustls"
|
||||
version = "0.27.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53"
|
||||
dependencies = [
|
||||
"http",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"rustls",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.21"
|
||||
@@ -1539,9 +1584,11 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"windows-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1844,6 +1891,36 @@ dependencies = [
|
||||
"windows-sys 0.45.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni"
|
||||
version = "0.22.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"combine",
|
||||
"jni-macros",
|
||||
"jni-sys 0.4.1",
|
||||
"log",
|
||||
"simd_cesu8",
|
||||
"thiserror 2.0.21",
|
||||
"walkdir",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni-macros"
|
||||
version = "0.22.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustc_version",
|
||||
"simd_cesu8",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni-sys"
|
||||
version = "0.3.1"
|
||||
@@ -1987,8 +2064,9 @@ checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae"
|
||||
|
||||
[[package]]
|
||||
name = "llembas-desktop"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"gtk",
|
||||
"libc",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -1997,6 +2075,7 @@ dependencies = [
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-opener",
|
||||
"tauri-plugin-single-instance",
|
||||
"tauri-plugin-updater",
|
||||
"ureq",
|
||||
]
|
||||
|
||||
@@ -2047,6 +2126,12 @@ version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "minisign-verify"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "871285dc19d8d0ebe0eef3d0e99a205f2a71363b122632cbbfa4a6c370a960ce"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
@@ -2312,6 +2397,18 @@ dependencies = [
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-osa-kit"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-quartz-core"
|
||||
version = "0.3.2"
|
||||
@@ -2392,6 +2489,12 @@ version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
@@ -2408,6 +2511,20 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "osakit"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
"objc2-osa-kit",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pango"
|
||||
version = "0.18.3"
|
||||
@@ -2813,15 +2930,20 @@ dependencies = [
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper-util",
|
||||
"js-sys",
|
||||
"log",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"rustls-platform-verifier",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tower-http",
|
||||
@@ -2920,11 +3042,23 @@ version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5"
|
||||
dependencies = [
|
||||
"openssl-probe",
|
||||
"openssl-probe 0.1.6",
|
||||
"rustls-pemfile",
|
||||
"rustls-pki-types",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
"security-framework 2.11.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-native-certs"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a073f5dc7469f984c52ad2752b63b0807745133b6de880b7b64c1ac4c48aec4"
|
||||
dependencies = [
|
||||
"openssl-probe 0.2.1",
|
||||
"rustls-pki-types",
|
||||
"schannel",
|
||||
"security-framework 3.7.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2945,6 +3079,33 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1167586491e2b18b8bfbb293e8180ec17c201c4f076d7cb3070ca964e7598f98"
|
||||
dependencies = [
|
||||
"core-foundation 0.10.1",
|
||||
"core-foundation-sys",
|
||||
"jni 0.22.4",
|
||||
"log",
|
||||
"once_cell",
|
||||
"rustls",
|
||||
"rustls-native-certs 0.8.5",
|
||||
"rustls-platform-verifier-android",
|
||||
"rustls-webpki",
|
||||
"security-framework 3.7.0",
|
||||
"security-framework-sys",
|
||||
"webpki-root-certs",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier-android"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.15"
|
||||
@@ -3050,6 +3211,19 @@ dependencies = [
|
||||
"security-framework-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "3.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"core-foundation 0.10.1",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework-sys"
|
||||
version = "2.17.0"
|
||||
@@ -3281,6 +3455,22 @@ version = "0.3.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
|
||||
|
||||
[[package]]
|
||||
name = "simd_cesu8"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520"
|
||||
dependencies = [
|
||||
"rustc_version",
|
||||
"simdutf8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simdutf8"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "1.0.4"
|
||||
@@ -3462,6 +3652,27 @@ dependencies = [
|
||||
"syn 3.0.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"core-foundation 0.9.4",
|
||||
"system-configuration-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration-sys"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-deps"
|
||||
version = "6.2.2"
|
||||
@@ -3493,7 +3704,7 @@ dependencies = [
|
||||
"gdkwayland-sys",
|
||||
"gdkx11-sys",
|
||||
"gtk",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"libc",
|
||||
"log",
|
||||
"ndk",
|
||||
@@ -3526,6 +3737,17 @@ dependencies = [
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tar"
|
||||
version = "0.4.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840"
|
||||
dependencies = [
|
||||
"filetime",
|
||||
"libc",
|
||||
"xattr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "target-lexicon"
|
||||
version = "0.12.16"
|
||||
@@ -3549,7 +3771,7 @@ dependencies = [
|
||||
"gtk",
|
||||
"heck 0.5.0",
|
||||
"http",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"libc",
|
||||
"log",
|
||||
"mime",
|
||||
@@ -3741,6 +3963,40 @@ dependencies = [
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-updater"
|
||||
version = "2.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "855a336eb389d2c1502244fd48fa2d8cb487a430f691bbaed025195f99a04489"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"dirs",
|
||||
"flate2",
|
||||
"futures-util",
|
||||
"http",
|
||||
"infer",
|
||||
"libc",
|
||||
"log",
|
||||
"minisign-verify",
|
||||
"osakit",
|
||||
"percent-encoding",
|
||||
"reqwest",
|
||||
"rustls",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tar",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"tempfile",
|
||||
"thiserror 2.0.21",
|
||||
"time",
|
||||
"tokio",
|
||||
"url",
|
||||
"windows-sys 0.61.2",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime"
|
||||
version = "2.12.1"
|
||||
@@ -3751,7 +4007,7 @@ dependencies = [
|
||||
"dpi",
|
||||
"gtk",
|
||||
"http",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"objc2",
|
||||
"objc2-ui-kit",
|
||||
"objc2-web-kit",
|
||||
@@ -3774,7 +4030,7 @@ checksum = "7f9e7e0c9f80a5130773cfabb6df4dc924518fb1b2c923726966804634b57d22"
|
||||
dependencies = [
|
||||
"gtk",
|
||||
"http",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"log",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
@@ -3963,6 +4219,16 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9cc2678c2cdd569ef8215e2afd7954ada2ae20b4fdd2c5fe6139a3b02d105db"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.20"
|
||||
@@ -4227,7 +4493,7 @@ dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"rustls-native-certs 0.7.3",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -4489,6 +4755,15 @@ dependencies = [
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-root-certs"
|
||||
version = "1.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.26.11"
|
||||
@@ -4671,6 +4946,17 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
@@ -4987,7 +5273,7 @@ dependencies = [
|
||||
"gtk",
|
||||
"http",
|
||||
"javascriptcore-rs",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"libc",
|
||||
"ndk",
|
||||
"objc2",
|
||||
@@ -5034,6 +5320,16 @@ dependencies = [
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xattr"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rustix",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.3"
|
||||
@@ -5187,6 +5483,18 @@ dependencies = [
|
||||
"syn 3.0.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "4.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"crc32fast",
|
||||
"indexmap 2.14.2",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zlib-rs"
|
||||
version = "0.6.8"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "llembas-desktop"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
description = "LLeMbas Desktop: your LLeMbas server in a window, with the agent on this computer"
|
||||
authors = ["Jaroslav Beneš"]
|
||||
license = "MIT"
|
||||
@@ -21,6 +21,7 @@ tauri = { version = "2", features = ["tray-icon", "unstable"] }
|
||||
tauri-plugin-dialog = "2"
|
||||
tauri-plugin-opener = "2"
|
||||
tauri-plugin-single-instance = "2"
|
||||
tauri-plugin-updater = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
ureq = { version = "2", default-features = false, features = ["tls", "native-certs", "json"] }
|
||||
@@ -28,6 +29,10 @@ ureq = { version = "2", default-features = false, features = ["tls", "native-cer
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
# The window's GTK box: Tauri packs every webview into it with an equal share on Linux.
|
||||
gtk = "0.18"
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
|
||||
+160
-3
@@ -26,11 +26,12 @@ use tauri::tray::TrayIconBuilder;
|
||||
use tauri::webview::WebviewBuilder;
|
||||
use tauri::window::{Color, WindowBuilder};
|
||||
use tauri::{
|
||||
AppHandle, Emitter, LogicalPosition, LogicalSize, Manager, PhysicalPosition, PhysicalSize,
|
||||
RunEvent, Url, Webview, WebviewUrl, Window, WindowEvent, Wry,
|
||||
AppHandle, Emitter, LogicalPosition, LogicalSize, Manager, RunEvent, Url, Webview, WebviewUrl,
|
||||
Window, WindowEvent, Wry,
|
||||
};
|
||||
use tauri_plugin_dialog::{DialogExt, MessageDialogKind};
|
||||
use tauri_plugin_opener::OpenerExt;
|
||||
use tauri_plugin_updater::UpdaterExt;
|
||||
|
||||
use agent::{Agent, State as AgentState};
|
||||
use settings::Settings;
|
||||
@@ -50,6 +51,8 @@ struct App {
|
||||
agent: Agent,
|
||||
tray: Mutex<Option<TrayItems>>,
|
||||
linking: Mutex<bool>,
|
||||
/// The newer release the last check found, if any.
|
||||
update: Mutex<Option<String>>,
|
||||
}
|
||||
|
||||
struct TrayItems {
|
||||
@@ -79,6 +82,8 @@ struct Status {
|
||||
linked: bool,
|
||||
agent: AgentState,
|
||||
agent_wanted: bool,
|
||||
/// A newer release of the app, when the last check found one.
|
||||
update: Option<String>,
|
||||
}
|
||||
|
||||
fn status(app: &App) -> Status {
|
||||
@@ -89,6 +94,7 @@ fn status(app: &App) -> Status {
|
||||
server: s.server,
|
||||
agent: app.agent.state(),
|
||||
agent_wanted: app.agent.wanted(),
|
||||
update: app.update.lock().unwrap().clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,6 +221,10 @@ fn build_main(handle: &AppHandle, target: Option<Url>) -> tauri::Result<()> {
|
||||
Some(u) => WebviewUrl::External(u),
|
||||
None => WebviewUrl::App("index.html".into()),
|
||||
};
|
||||
#[cfg(target_os = "linux")]
|
||||
if let Some(bar) = handle.get_webview(TITLEBAR) {
|
||||
pin_titlebar(&bar);
|
||||
}
|
||||
let nav = handle.clone();
|
||||
window.add_child(
|
||||
WebviewBuilder::new(CONTENT, url).on_navigation(move |url| allowed(&nav, url)),
|
||||
@@ -224,12 +234,45 @@ fn build_main(handle: &AppHandle, target: Option<Url>) -> tauri::Result<()> {
|
||||
(size.height - TITLE_H - 2.0 * FRAME).max(0.0),
|
||||
),
|
||||
)?;
|
||||
layout(&window);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// On Linux, Tauri packs every webview of a window into the window's GTK box, each with an equal
|
||||
/// share of the height, and positions and sizes set on a webview are not applied there. So the
|
||||
/// title bar is told directly: a fixed height, no share of what is left (the content takes it).
|
||||
#[cfg(target_os = "linux")]
|
||||
fn pin_titlebar(bar: &Webview) {
|
||||
let _ = bar.with_webview(|pv| {
|
||||
use gtk::prelude::*;
|
||||
let view = pv.inner();
|
||||
view.set_size_request(-1, TITLE_H as i32);
|
||||
if let Some(parent) = view.parent().and_then(|p| p.downcast::<gtk::Box>().ok()) {
|
||||
parent.set_child_packing(&view, false, false, 0, gtk::PackType::Start);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Lay the two webviews out in the window: the title bar on top, the content under it, and the
|
||||
/// frame around both unless the window is maximized (then there is nothing to resize).
|
||||
fn layout(window: &Window) {
|
||||
let framed =
|
||||
!(window.is_maximized().unwrap_or(false) || window.is_fullscreen().unwrap_or(false));
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
use gtk::prelude::*;
|
||||
// The box's border is the frame: the window's own surface, which GTK resizes by.
|
||||
if let Ok(vbox) = window.default_vbox() {
|
||||
vbox.set_border_width(if framed { FRAME as u32 } else { 0 });
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
place(window, framed);
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn place(window: &Window, framed: bool) {
|
||||
use tauri::{PhysicalPosition, PhysicalSize};
|
||||
let handle = window.app_handle();
|
||||
let (Some(bar), Some(body)) = (handle.get_webview(TITLEBAR), handle.get_webview(CONTENT))
|
||||
else {
|
||||
@@ -421,6 +464,110 @@ fn build_tray(handle: &AppHandle) -> tauri::Result<TrayItems> {
|
||||
Ok(TrayItems { status, toggle })
|
||||
}
|
||||
|
||||
/// How often a running app looks for a new release (and once shortly after it starts).
|
||||
const UPDATE_EVERY: std::time::Duration = std::time::Duration::from_secs(6 * 3600);
|
||||
|
||||
/// Ask the update manifest whether a newer release exists; remember and announce the answer.
|
||||
/// The manifest and each package are signed with the updater key, which the app checks before
|
||||
/// installing anything.
|
||||
async fn check_update(handle: &AppHandle) -> Result<Option<String>, String> {
|
||||
let found = handle
|
||||
.updater()
|
||||
.map_err(|e| e.to_string())?
|
||||
.check()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
.map(|u| u.version);
|
||||
*handle.state::<App>().update.lock().unwrap() = found.clone();
|
||||
let _ = handle.emit("status", status_of(handle));
|
||||
Ok(found)
|
||||
}
|
||||
|
||||
fn watch_updates(handle: AppHandle) {
|
||||
std::thread::spawn(move || {
|
||||
std::thread::sleep(std::time::Duration::from_secs(20));
|
||||
loop {
|
||||
let _ = tauri::async_runtime::block_on(check_update(&handle));
|
||||
std::thread::sleep(UPDATE_EVERY);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Download and install the newer release, then start again on it. A .deb is installed with
|
||||
/// the system's password prompt (pkexec); an AppImage replaces itself.
|
||||
async fn install_update(handle: AppHandle) -> Result<(), String> {
|
||||
let update = handle
|
||||
.updater()
|
||||
.map_err(|e| e.to_string())?
|
||||
.check()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
.ok_or("LLeMbas is up to date.")?;
|
||||
update
|
||||
.download_and_install(|_, _| {}, || {})
|
||||
.await
|
||||
.map_err(|e| format!("The update could not be installed: {e}"))?;
|
||||
handle.state::<App>().agent.stop();
|
||||
handle.restart()
|
||||
}
|
||||
|
||||
fn offer_update(h: &AppHandle) {
|
||||
let h = h.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let version = match check_update(&h).await {
|
||||
Ok(Some(v)) => v,
|
||||
Ok(None) => {
|
||||
h.dialog()
|
||||
.message(format!(
|
||||
"LLeMbas {} is the newest release.",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
))
|
||||
.title("LLeMbas is up to date")
|
||||
.show(|_| {});
|
||||
return;
|
||||
}
|
||||
Err(e) => {
|
||||
h.dialog()
|
||||
.message(format!("Could not look for updates: {e}"))
|
||||
.title("LLeMbas")
|
||||
.kind(MessageDialogKind::Warning)
|
||||
.show(|_| {});
|
||||
return;
|
||||
}
|
||||
};
|
||||
let again = h.clone();
|
||||
h.dialog()
|
||||
.message(format!(
|
||||
"LLeMbas {version} is available (this is {}). Install it now? LLeMbas starts again afterwards; the agent stops for a moment.",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
))
|
||||
.title("Update LLeMbas")
|
||||
.buttons(tauri_plugin_dialog::MessageDialogButtons::OkCancelCustom(
|
||||
"Install".into(),
|
||||
"Later".into(),
|
||||
))
|
||||
.show(move |yes| {
|
||||
if yes {
|
||||
tauri::async_runtime::spawn(async move {
|
||||
if let Err(e) = install_update(again.clone()).await {
|
||||
again
|
||||
.dialog()
|
||||
.message(e)
|
||||
.title("LLeMbas")
|
||||
.kind(MessageDialogKind::Error)
|
||||
.show(|_| {});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn update_app(handle: AppHandle) {
|
||||
offer_update(&handle);
|
||||
}
|
||||
|
||||
/// A menu item chosen, from the tray or from the title bar's menu: the same ids in both.
|
||||
fn on_menu(h: &AppHandle, id: &str) {
|
||||
match id {
|
||||
@@ -442,6 +589,7 @@ fn on_menu(h: &AppHandle, id: &str) {
|
||||
.show(|_| {});
|
||||
}
|
||||
}
|
||||
"update" => offer_update(h),
|
||||
"reload" => {
|
||||
if let Some(c) = content(h) {
|
||||
if let Ok(url) = c.url() {
|
||||
@@ -479,6 +627,10 @@ fn titlebar_menu(window: Window) -> Result<(), String> {
|
||||
true,
|
||||
)?;
|
||||
let link = m("link", "Link this computer…", true)?;
|
||||
let update = match app.update.lock().unwrap().clone() {
|
||||
Some(v) => m("update", &format!("Install LLeMbas {v}…"), true)?,
|
||||
None => m("update", "Check for updates…", true)?,
|
||||
};
|
||||
let reload = m("reload", "Reload", true)?;
|
||||
let server = m("server", "Change server…", true)?;
|
||||
let quit = m("quit", "Quit LLeMbas", true)?;
|
||||
@@ -490,6 +642,7 @@ fn titlebar_menu(window: Window) -> Result<(), String> {
|
||||
&link,
|
||||
&PredefinedMenuItem::separator(h)?,
|
||||
&reload,
|
||||
&update,
|
||||
&server,
|
||||
&PredefinedMenuItem::separator(h)?,
|
||||
&quit,
|
||||
@@ -518,12 +671,14 @@ pub fn run() {
|
||||
.plugin(tauri_plugin_single_instance::init(|h, _argv, _cwd| show(h)))
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
get_status,
|
||||
set_server,
|
||||
set_agent,
|
||||
link_computer,
|
||||
titlebar_menu
|
||||
titlebar_menu,
|
||||
update_app
|
||||
])
|
||||
.on_menu_event(|h, ev| on_menu(h, ev.id().as_ref()))
|
||||
.setup(|app| {
|
||||
@@ -547,10 +702,12 @@ pub fn run() {
|
||||
agent,
|
||||
tray: Mutex::new(None),
|
||||
linking: Mutex::new(false),
|
||||
update: Mutex::new(None),
|
||||
});
|
||||
let items = build_tray(&handle)?;
|
||||
*handle.state::<App>().tray.lock().unwrap() = Some(items);
|
||||
open_main(&handle, loaded.server.as_deref())?;
|
||||
watch_updates(handle.clone());
|
||||
if let (true, Some(server)) = (loaded.agent, loaded.server) {
|
||||
handle.state::<App>().agent.start(server);
|
||||
}
|
||||
|
||||
@@ -47,5 +47,14 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"updater": {
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXkgMzNBNUJCMkNCQTBCOTEwRgpSV1FQa1F1NkxMdWxNOCtnT2NMQU5QOTBHZ3hUaC9UNHVFZThUR2lqOGdWWUVObi9mbU00amoyago=",
|
||||
"endpoints": [
|
||||
"https://llembas.eu/desktop/latest.json"
|
||||
],
|
||||
"requireSignedVersion": true
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
-2
@@ -15,8 +15,8 @@ html, body {
|
||||
|
||||
.bar {
|
||||
display: grid;
|
||||
/* leaf · name · host (takes the rest, and is the drag area) · agent · menu · sep · 3 controls */
|
||||
grid-template-columns: var(--titlebar-h) auto minmax(0, 1fr) auto var(--titlebar-h) 1px repeat(3, 2.875rem);
|
||||
/* leaf · name · host (takes the rest, and is the drag area) · update · agent · menu · sep · 3 controls */
|
||||
grid-template-columns: var(--titlebar-h) auto minmax(0, 1fr) auto auto var(--titlebar-h) 1px repeat(3, 2.875rem);
|
||||
align-items: center;
|
||||
height: var(--titlebar-h);
|
||||
border-bottom: 1px solid var(--border);
|
||||
@@ -66,6 +66,15 @@ button:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
|
||||
.bar__agent[data-state="failed"] .dot,
|
||||
.bar__agent[data-state="not_linked"] .dot { background: var(--warning); }
|
||||
|
||||
.bar__update {
|
||||
padding: 0 var(--sp-3);
|
||||
color: var(--accent-ink);
|
||||
background: var(--accent);
|
||||
}
|
||||
.bar__update:hover { color: var(--accent-ink); background: var(--accent-hover); }
|
||||
/* Hidden keeps its grid track (auto, so zero wide): the slots after it never move. */
|
||||
.bar__update[hidden] { display: block; width: 0; padding: 0; overflow: hidden; }
|
||||
|
||||
.bar__sep { width: 1px; height: 1.25rem; background: var(--border); }
|
||||
|
||||
.bar__btn { display: grid; place-items: center; }
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
<span class="bar__name" data-tauri-drag-region><b>LL</b>e<b>M</b>bas</span>
|
||||
<span class="bar__host" id="host" data-tauri-drag-region></span>
|
||||
|
||||
<button class="bar__update" id="update" type="button" hidden>update</button>
|
||||
<button class="bar__agent" id="agent" type="button" title="Agent">
|
||||
<span class="dot" aria-hidden="true"></span><span id="agent-text">agent off</span>
|
||||
</button>
|
||||
|
||||
@@ -29,6 +29,9 @@ function render(s) {
|
||||
$("agent").dataset.state = state
|
||||
$("agent-text").textContent = WORDS[state] ?? state
|
||||
$("agent").title = state === "failed" ? `Agent stopped: ${s.agent.detail}` : "Agent"
|
||||
$("update").hidden = !s.update
|
||||
$("update").textContent = s.update ? `update ${s.update}` : "update"
|
||||
$("update").title = s.update ? `LLeMbas ${s.update} is available (this is ${s.version})` : ""
|
||||
}
|
||||
|
||||
function maximized(on) {
|
||||
@@ -41,6 +44,7 @@ function maximized(on) {
|
||||
$("minimize").addEventListener("click", () => win.minimize())
|
||||
$("maximize").addEventListener("click", () => win.toggleMaximize())
|
||||
$("close").addEventListener("click", () => win.close())
|
||||
$("update").addEventListener("click", () => invoke("update_app"))
|
||||
$("menu").addEventListener("click", () => invoke("titlebar_menu"))
|
||||
$("agent").addEventListener("click", () => invoke("titlebar_menu"))
|
||||
|
||||
|
||||
Reference in new issue
Block a user