From e69a61d81a0c16c16aeaff0f9319cf5edc20c27c Mon Sep 17 00:00:00 2001 From: Subhan Gadirli Date: Wed, 12 Aug 2026 19:59:23 +0400 Subject: [PATCH 1/5] fix: sync app light/dark mode with the system theme preference Plain GTK4 (without libadwaita) does not follow the desktop's dark-mode preference on its own. Read org.freedesktop.appearance color-scheme from the XDG Desktop Portal at startup and apply it via gtk-application-prefer-dark-theme, then keep following live changes via the SettingChanged signal. xdg-desktop-portal-gnome wraps the reported value in an extra GVariant "v" layer beyond what the portal spec calls for, so unwrap variant layers in a loop rather than once. --- src/ui/style.rs | 97 ++++++++++++++++++++++++++++++++++++++++++++++++ src/ui/window.rs | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/ui/style.rs b/src/ui/style.rs index b45962f..a0a33a2 100644 --- a/src/ui/style.rs +++ b/src/ui/style.rs @@ -1,5 +1,102 @@ use gtk4::CssProvider; use gtk4::gdk; +use gtk4::gio; +use gtk4::gio::prelude::*; + +const PORTAL_BUS_NAME: &str = "org.freedesktop.portal.Desktop"; +const PORTAL_OBJECT_PATH: &str = "/org/freedesktop/portal/desktop"; +const PORTAL_SETTINGS_IFACE: &str = "org.freedesktop.portal.Settings"; +const APPEARANCE_NAMESPACE: &str = "org.freedesktop.appearance"; +const COLOR_SCHEME_KEY: &str = "color-scheme"; + +/// Peels away GVariant `v` (variant) wrapper layers until a non-variant value +/// is reached. Some portal backends (notably xdg-desktop-portal-gnome) wrap +/// the `Read`/`SettingChanged` payload in an extra variant layer on top of +/// the `v` already mandated by the portal spec, so a single unwrap isn't +/// always enough. +fn unwrap_variant(mut value: gtk4::glib::Variant) -> gtk4::glib::Variant { + while let Some(inner) = value.as_variant() { + value = inner; + } + value +} + +/// Maps the portal's `color-scheme` value (0 = no preference, 1 = prefer +/// dark, 2 = prefer light) to a `prefer-dark-theme` bool, if it expresses one. +fn color_scheme_prefers_dark(value: u32) -> Option { + match value { + 1 => Some(true), + 2 => Some(false), + _ => None, + } +} + +fn apply_color_scheme(value: u32) { + tracing::debug!(value, "portal color-scheme received"); + match color_scheme_prefers_dark(value) { + Some(prefer_dark) => match gtk4::Settings::default() { + Some(settings) => { + settings.set_gtk_application_prefer_dark_theme(prefer_dark); + tracing::debug!( + prefer_dark, + now = settings.is_gtk_application_prefer_dark_theme(), + "applied gtk-application-prefer-dark-theme" + ); + } + None => tracing::warn!("no default GtkSettings available (no display?)"), + }, + None => tracing::debug!(value, "color-scheme value expresses no preference"), + } +} + +/// Syncs the app's light/dark mode with the desktop's system-wide preference +/// via the XDG Desktop Portal, since plain GTK4 (without libadwaita) does not +/// do this on its own. Keeps following live changes for the rest of the run. +pub fn init_theme_sync() { + let proxy = match gio::DBusProxy::for_bus_sync( + gio::BusType::Session, + gio::DBusProxyFlags::NONE, + None, + PORTAL_BUS_NAME, + PORTAL_OBJECT_PATH, + PORTAL_SETTINGS_IFACE, + gio::Cancellable::NONE, + ) { + Ok(proxy) => proxy, + Err(err) => { + tracing::warn!(%err, "could not connect to xdg-desktop-portal Settings interface"); + return; + } + }; + + match proxy.call_sync( + "Read", + Some(&(APPEARANCE_NAMESPACE, COLOR_SCHEME_KEY).to_variant()), + gio::DBusCallFlags::NONE, + 1000, + gio::Cancellable::NONE, + ) { + Ok(reply) => match unwrap_variant(reply.child_value(0)).get::() { + Some(value) => apply_color_scheme(value), + None => tracing::warn!(reply = %reply, "unexpected reply shape from portal Read"), + }, + Err(err) => tracing::warn!(%err, "portal Settings.Read call failed"), + } + + proxy.connect_g_signal(move |_proxy, _sender, signal, params| { + if signal != "SettingChanged" { + return; + } + if let Some((namespace, key, value)) = params.get::<(String, String, gtk4::glib::Variant)>() + { + if namespace == APPEARANCE_NAMESPACE && key == COLOR_SCHEME_KEY { + if let Some(value) = unwrap_variant(value).get::() { + apply_color_scheme(value); + } + } + } + }); +} pub fn init_style() { let provider = CssProvider::new(); diff --git a/src/ui/window.rs b/src/ui/window.rs index ab2032a..f113868 100644 --- a/src/ui/window.rs +++ b/src/ui/window.rs @@ -8,7 +8,7 @@ use crate::ui::file_explorer::FileExplorer; use crate::ui::monitor::SystemMonitor; use crate::ui::server_list::{ServerAction, ServerList}; use crate::ui::ssh_keys::build_ssh_keys_ui; -use crate::ui::style::init_style; +use crate::ui::style::{init_style, init_theme_sync}; use gtk4::prelude::*; use gtk4::{gdk, gio, glib}; use std::cell::RefCell; @@ -29,6 +29,7 @@ struct AppWindowInner { impl AppWindow { pub fn new(app: >k4::Application) -> Self { + init_theme_sync(); init_style(); let window = gtk4::ApplicationWindow::builder() .application(app) From 5ce240e1618abff5e1cca97f33d8fe8314386285 Mon Sep 17 00:00:00 2001 From: Subhan Gadirli Date: Wed, 12 Aug 2026 20:00:12 +0400 Subject: [PATCH 2/5] packaging: add Fedora RPM spec and build script Adds packages/rpm/rustmius.spec plus a build-rpm.sh helper mirroring build-deb.sh: it snapshots the working tree into a source tarball, builds in a private rpmbuild _topdir, and copies the resulting RPMs into dist/. Runtime deps (gtk4, vte291-gtk4, openssl, etc.) are picked up automatically from the linked binary via RPM's find-requires. --- packages/rpm/build-rpm.sh | 48 ++++++++++++++++++++++ packages/rpm/rustmius.spec | 83 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100755 packages/rpm/build-rpm.sh create mode 100644 packages/rpm/rustmius.spec diff --git a/packages/rpm/build-rpm.sh b/packages/rpm/build-rpm.sh new file mode 100755 index 0000000..0724c1e --- /dev/null +++ b/packages/rpm/build-rpm.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# +# Build a .rpm package for Rustmius (Fedora / RHEL / openSUSE family). +# +# Usage: +# packages/rpm/build-rpm.sh +# +# Output: dist/rustmius--..rpm (in the repo root) +# +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +cd "$REPO_ROOT" + +PKG_NAME="rustmius" +VERSION="$(grep -m1 '^version' Cargo.toml | sed -E 's/.*"(.*)".*/\1/')" + +command -v rpmbuild >/dev/null 2>&1 || { + echo "!! rpmbuild not found. Install it with: sudo dnf install rpm-build rpmdevtools" >&2 + exit 1 +} + +echo ">> Packaging $PKG_NAME $VERSION (.rpm)" + +# Private rpmbuild tree so this never touches ~/rpmbuild. +TOPDIR="$(mktemp -d)" +trap 'rm -rf "$TOPDIR"' EXIT +mkdir -p "$TOPDIR"/{BUILD,RPMS,SOURCES,SPECS,SRPMS} + +# Snapshot the working tree (including uncommitted edits, like build-deb.sh +# building straight from target/release) into a source tarball. Name matches +# the spec's %autosetup -n rustmius-$VERSION. +tar --exclude=.git --exclude=target --exclude=dist \ + --transform "s,^,$PKG_NAME-$VERSION/," \ + -czf "$TOPDIR/SOURCES/$PKG_NAME-$VERSION.tar.gz" -- * + +cp "packages/rpm/$PKG_NAME.spec" "$TOPDIR/SPECS/" + +rpmbuild --define "_topdir $TOPDIR" -ba "$TOPDIR/SPECS/$PKG_NAME.spec" + +mkdir -p dist +find "$TOPDIR/RPMS" -name '*.rpm' -exec cp -v {} dist/ \; +find "$TOPDIR/SRPMS" -name '*.rpm' -exec cp -v {} dist/ \; + +echo "" +echo ">> Built RPM(s) in dist/:" +ls -1 dist/*.rpm diff --git a/packages/rpm/rustmius.spec b/packages/rpm/rustmius.spec new file mode 100644 index 0000000..cd966dc --- /dev/null +++ b/packages/rpm/rustmius.spec @@ -0,0 +1,83 @@ +Name: rustmius +Version: 2.5.0 +Release: 3%{?dist} +Summary: Local Termius alternative for Linux (GTK4) + +License: AGPL-3.0-or-later +URL: https://github.com/Cleboost/Rustmius +# Generated locally via `git archive`, see packages/rpm/build-rpm.sh +Source0: %{name}-%{version}.tar.gz + +BuildRequires: cargo +BuildRequires: rust +BuildRequires: gcc +BuildRequires: desktop-file-utils +BuildRequires: pkgconfig(gtk4) >= 4.12 +BuildRequires: pkgconfig(vte-2.91-gtk4) +BuildRequires: pkgconfig(libssh2) +BuildRequires: pkgconfig(openssl) +BuildRequires: pkgconfig(zlib) + +# Runtime deps are also picked up automatically from the linked binary +# (soname-based Requires), these just make the intent explicit. +Requires: gtk4%{?_isa} >= 4.12 +Requires: vte291-gtk4%{?_isa} +Requires: hicolor-icon-theme + +%description +Rustmius is a modern, fast, and fully local alternative to Termius, +built with Rust and GTK4. It provides an integrated SSH terminal +(via VTE), an advanced SFTP explorer with drag & drop, a host +manager, and secure secret storage through the system keyring. + +%prep +%autosetup -n %{name}-%{version} + +%build +cargo build --release --locked + +%install +install -Dm0755 target/release/%{name} %{buildroot}%{_bindir}/%{name} + +install -Dm0644 packages/org.rustmius.Rustmius.desktop \ + %{buildroot}%{_datadir}/applications/org.rustmius.Rustmius.desktop + +install -dm0755 %{buildroot}%{_datadir}/icons/hicolor/512x512/apps +if command -v magick >/dev/null 2>&1; then + magick packages/rustmius.png -resize 512x512 \ + %{buildroot}%{_datadir}/icons/hicolor/512x512/apps/%{name}.png +elif command -v convert >/dev/null 2>&1; then + convert packages/rustmius.png -resize 512x512 \ + %{buildroot}%{_datadir}/icons/hicolor/512x512/apps/%{name}.png +else + install -m0644 packages/rustmius.png \ + %{buildroot}%{_datadir}/icons/hicolor/512x512/apps/%{name}.png +fi + +install -dm0755 %{buildroot}%{_mandir}/man1 +sed "s/@VERSION@/%{version}/g" packages/deb/rustmius.1 \ + > %{buildroot}%{_mandir}/man1/%{name}.1 + +%check +desktop-file-validate %{buildroot}%{_datadir}/applications/org.rustmius.Rustmius.desktop + +%files +%license LICENSE +%doc README.md +%{_bindir}/%{name} +%{_datadir}/applications/org.rustmius.Rustmius.desktop +%{_datadir}/icons/hicolor/512x512/apps/%{name}.png +%{_mandir}/man1/%{name}.1* + +%changelog +* Wed Aug 12 2026 Subhan Gadirli - 2.5.0-3 +- Fix dark-mode sync: xdg-desktop-portal-gnome double-wraps the + Settings.Read/SettingChanged value in an extra GVariant "v" layer, + which needs peeling before reading the color-scheme uint32. + +* Wed Aug 12 2026 Subhan Gadirli - 2.5.0-2 +- Sync app light/dark mode with the system preference via the XDG + Desktop Portal (plain GTK4 does not do this on its own). + +* Wed Aug 12 2026 Subhan Gadirli - 2.5.0-1 +- Initial RPM packaging for Fedora. From 653abc74798e878161d37f2c7eac3c956260e575 Mon Sep 17 00:00:00 2001 From: subhangadirli <222268917+subhangadirli@users.noreply.github.com> Date: Wed, 12 Aug 2026 16:08:23 +0000 Subject: [PATCH 3/5] fix: apply clippy suggestions --- src/ui/style.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/ui/style.rs b/src/ui/style.rs index a0a33a2..d9de50e 100644 --- a/src/ui/style.rs +++ b/src/ui/style.rs @@ -88,13 +88,10 @@ pub fn init_theme_sync() { return; } if let Some((namespace, key, value)) = params.get::<(String, String, gtk4::glib::Variant)>() - { - if namespace == APPEARANCE_NAMESPACE && key == COLOR_SCHEME_KEY { - if let Some(value) = unwrap_variant(value).get::() { + && namespace == APPEARANCE_NAMESPACE && key == COLOR_SCHEME_KEY + && let Some(value) = unwrap_variant(value).get::() { apply_color_scheme(value); } - } - } }); } From ef9ee3d3fa4f6436025796ec840949421b48da91 Mon Sep 17 00:00:00 2001 From: Subhan Gadirli Date: Wed, 12 Aug 2026 20:20:38 +0400 Subject: [PATCH 4/5] ci: build and release a .rpm alongside the existing .deb Adds an rpm job to the release workflow, mirroring the deb job: it runs inside a fedora:latest container, reuses the prebuilt generic x86_64 binary artifact via SKIP_BUILD=1, and uploads the resulting package for the release job to attach to the GitHub release. To support SKIP_BUILD, build-rpm.sh now stages a prebuilt binary into the source tarball when present, and the spec's %build step skips `cargo build` when it finds one. Also disables Fedora's debuginfo/ debugsource subpackages (%global debug_package %{nil}): every release channel builds with `strip = "symbols"`, so there's no debug info for find-debuginfo to extract from a prebuilt binary anyway. Also ignores stray *.rpm build output and rpmbuild/ trees, matching the existing deb/AUR artifact ignores. --- .github/workflows/release.yml | 53 ++++++++++++++++++++++++++++++++++- .gitignore | 2 ++ packages/rpm/build-rpm.sh | 23 +++++++++++---- packages/rpm/rustmius.spec | 20 +++++++++++-- 4 files changed, 88 insertions(+), 10 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7d39590..69eac62 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -149,9 +149,54 @@ jobs: name: deb path: dist/*.deb + rpm: + name: Build .rpm (Fedora) + needs: [prepare, build] + runs-on: ubuntu-latest + container: fedora:latest + permissions: + contents: read + steps: + - name: Install git (required by actions/checkout) + run: dnf install -y git tar + + - name: Checkout code + uses: actions/checkout@v7 + with: + ref: ${{ needs.prepare.outputs.sha }} + + - name: Install packaging dependencies + run: | + dnf install -y rpm-build rpmdevtools gtk4-devel vte291-gtk4-devel \ + libssh2-devel openssl-devel zlib-devel ImageMagick + + - name: Download prebuilt generic binary + uses: actions/download-artifact@v8 + with: + name: binary-x86_64 + path: prebuilt + + - name: Stage binary for packaging + run: | + mkdir -p target/release + install -m755 prebuilt/rustmius-x86_64 target/release/rustmius + + - name: Build .rpm package + env: + SKIP_BUILD: "1" + run: ./packages/rpm/build-rpm.sh + + - name: Upload .rpm artifact + uses: actions/upload-artifact@v7 + with: + name: rpm + path: | + dist/*.rpm + !dist/*.src.rpm + release: name: Create Release - needs: [prepare, build, deb] + needs: [prepare, build, deb, rpm] runs-on: ubuntu-latest permissions: contents: write @@ -169,6 +214,12 @@ jobs: name: deb path: artifacts + - name: Download .rpm artifact + uses: actions/download-artifact@v8 + with: + name: rpm + path: artifacts + - name: Release uses: softprops/action-gh-release@v3 with: diff --git a/.gitignore b/.gitignore index 28bc460..00dd105 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ packages/**/src/ packages/**/*.tar.gz packages/**/*.tar.zst packages/**/*.pkg.tar.* +packages/**/*.rpm +rpmbuild/ diff --git a/packages/rpm/build-rpm.sh b/packages/rpm/build-rpm.sh index 0724c1e..c38439a 100755 --- a/packages/rpm/build-rpm.sh +++ b/packages/rpm/build-rpm.sh @@ -3,7 +3,8 @@ # Build a .rpm package for Rustmius (Fedora / RHEL / openSUSE family). # # Usage: -# packages/rpm/build-rpm.sh +# packages/rpm/build-rpm.sh # builds release binary if missing, then packages +# SKIP_BUILD=1 packages/rpm/build-rpm.sh # reuse an existing target/release/rustmius # # Output: dist/rustmius--..rpm (in the repo root) # @@ -25,19 +26,29 @@ echo ">> Packaging $PKG_NAME $VERSION (.rpm)" # Private rpmbuild tree so this never touches ~/rpmbuild. TOPDIR="$(mktemp -d)" -trap 'rm -rf "$TOPDIR"' EXIT +STAGE="$(mktemp -d)" +trap 'rm -rf "$TOPDIR" "$STAGE"' EXIT mkdir -p "$TOPDIR"/{BUILD,RPMS,SOURCES,SPECS,SRPMS} # Snapshot the working tree (including uncommitted edits, like build-deb.sh # building straight from target/release) into a source tarball. Name matches # the spec's %autosetup -n rustmius-$VERSION. -tar --exclude=.git --exclude=target --exclude=dist \ - --transform "s,^,$PKG_NAME-$VERSION/," \ - -czf "$TOPDIR/SOURCES/$PKG_NAME-$VERSION.tar.gz" -- * +SRC_DIR="$STAGE/$PKG_NAME-$VERSION" +mkdir -p "$SRC_DIR" +tar --exclude=.git --exclude=target --exclude=dist -cf - -- * | tar -xf - -C "$SRC_DIR" + +# With SKIP_BUILD=1, carry the already-built binary into the tarball so the +# spec's %build step can skip its own `cargo build` (mirrors build-deb.sh). +if [[ "${SKIP_BUILD:-0}" == "1" && -x "target/release/$PKG_NAME" ]]; then + echo ">> Reusing prebuilt target/release/$PKG_NAME (SKIP_BUILD=1)" + install -Dm755 "target/release/$PKG_NAME" "$SRC_DIR/target/release/$PKG_NAME" +fi + +tar -czf "$TOPDIR/SOURCES/$PKG_NAME-$VERSION.tar.gz" -C "$STAGE" "$PKG_NAME-$VERSION" cp "packages/rpm/$PKG_NAME.spec" "$TOPDIR/SPECS/" -rpmbuild --define "_topdir $TOPDIR" -ba "$TOPDIR/SPECS/$PKG_NAME.spec" +SKIP_BUILD="${SKIP_BUILD:-0}" rpmbuild --define "_topdir $TOPDIR" -ba "$TOPDIR/SPECS/$PKG_NAME.spec" mkdir -p dist find "$TOPDIR/RPMS" -name '*.rpm' -exec cp -v {} dist/ \; diff --git a/packages/rpm/rustmius.spec b/packages/rpm/rustmius.spec index cd966dc..3faccf4 100644 --- a/packages/rpm/rustmius.spec +++ b/packages/rpm/rustmius.spec @@ -1,11 +1,17 @@ +# Release binaries are built with `strip = "symbols"` (see [profile.release] +# in Cargo.toml) across every packaging channel (deb, AUR, GitHub releases), +# so there is no debug info here for Fedora's debuginfo/debugsource split to +# extract in the first place. +%global debug_package %{nil} + Name: rustmius Version: 2.5.0 -Release: 3%{?dist} +Release: 4%{?dist} Summary: Local Termius alternative for Linux (GTK4) License: AGPL-3.0-or-later URL: https://github.com/Cleboost/Rustmius -# Generated locally via `git archive`, see packages/rpm/build-rpm.sh +# Generated locally from the working tree, see packages/rpm/build-rpm.sh Source0: %{name}-%{version}.tar.gz BuildRequires: cargo @@ -34,7 +40,11 @@ manager, and secure secret storage through the system keyring. %autosetup -n %{name}-%{version} %build -cargo build --release --locked +if [ "${SKIP_BUILD:-0}" = "1" ] && [ -x target/release/%{name} ]; then + echo ">> Reusing prebuilt %{name} binary (SKIP_BUILD=1)" +else + cargo build --release --locked +fi %install install -Dm0755 target/release/%{name} %{buildroot}%{_bindir}/%{name} @@ -70,6 +80,10 @@ desktop-file-validate %{buildroot}%{_datadir}/applications/org.rustmius.Rustmius %{_mandir}/man1/%{name}.1* %changelog +* Wed Aug 12 2026 Subhan Gadirli - 2.5.0-4 +- Support SKIP_BUILD=1 to reuse a prebuilt binary (used by CI to avoid + recompiling for each package format). + * Wed Aug 12 2026 Subhan Gadirli - 2.5.0-3 - Fix dark-mode sync: xdg-desktop-portal-gnome double-wraps the Settings.Read/SettingChanged value in an extra GVariant "v" layer, From 869173889be9d8aa7f2ef0561f30830ad4133412 Mon Sep 17 00:00:00 2001 From: subhangadirli <222268917+subhangadirli@users.noreply.github.com> Date: Wed, 12 Aug 2026 16:24:38 +0000 Subject: [PATCH 5/5] style: apply rustfmt --- src/ui/style.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/ui/style.rs b/src/ui/style.rs index d9de50e..3a96786 100644 --- a/src/ui/style.rs +++ b/src/ui/style.rs @@ -88,10 +88,12 @@ pub fn init_theme_sync() { return; } if let Some((namespace, key, value)) = params.get::<(String, String, gtk4::glib::Variant)>() - && namespace == APPEARANCE_NAMESPACE && key == COLOR_SCHEME_KEY - && let Some(value) = unwrap_variant(value).get::() { - apply_color_scheme(value); - } + && namespace == APPEARANCE_NAMESPACE + && key == COLOR_SCHEME_KEY + && let Some(value) = unwrap_variant(value).get::() + { + apply_color_scheme(value); + } }); }