diff --git a/docs/operations.md b/docs/operations.md index f583a269..6c3738c1 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -623,6 +623,6 @@ their isolated offscreen process. The power saver profile is a reduced-motion request too, for as long as it is on; it changes neither this variable nor **Settings → Appearance → Reduce -motion**. The compositor follows it as well: blur drops to one pass and +motion**. The compositor follows it as well: it uses one blur pass and Hyprland animations turn off, and the values it replaced return when power saver ends. diff --git a/docs/quickshell-notes.md b/docs/quickshell-notes.md index d61062b2..740ed244 100644 --- a/docs/quickshell-notes.md +++ b/docs/quickshell-notes.md @@ -646,10 +646,14 @@ glass and detached panels. What that added, and what it needs: artwork and functional shapes such as switch tracks and progress meters retain their own presentation. Update assets with `scripts/update-tabler-icons`; see `assets/tabler/README.md`. -- **Blur is the compositor's.** `roles/desktop/files/looknfeel.lua` exports the - named `quickshell_blur_rule` matching the `qs-*` namespaces. The Appearance - switch calls that handle through `hyprctl eval`; its initial `enabled` value - is read from the persisted JSON so compositor reloads retain the choice. +- **Blur is the compositor's.** `roles/desktop/templates/looknfeel.lua.j2` + defaults to one blur pass to reduce desktop GPU work while keeping + animations and display refresh rates unchanged. Both Ansible deployment + and the ISO desktop package render this template. + It exports the named `quickshell_blur_rule` matching the `qs-*` namespaces. + The Appearance switch calls that handle through `hyprctl eval`; its initial + `enabled` value is read from the persisted JSON so compositor reloads retain + the choice. Layer namespaces stay fixed because changing one after a Wayland surface is connected does not update the compositor rule safely. - **Nothing that floats over the desktop may draw a drop shadow.** Blur is diff --git a/docs/remote-server-widget.md b/docs/remote-server-widget.md index fa216ab2..3985f34c 100644 --- a/docs/remote-server-widget.md +++ b/docs/remote-server-widget.md @@ -1,10 +1,14 @@ # Remote Server widget -Add **Remote Server** from Settings → Menubar → Widgets, then open its options. -Set **SSH host** to an existing SSH alias or `user@hostname`, and optionally set -a display name. For example: `john@10.10.0.7`, **The Beast**. These are personal -settings, not distribution defaults; new installations leave the widget disabled -and its host empty, and its dashboard offers **Choose SSH host** until one is set. +**Remote Server** is included and enabled in the menubar on new installations, +including ISO installations. Its host and display name start empty: the bar +shows **Set up** and the dashboard offers **Choose SSH host**. No SSH connection +starts until a host is configured. Existing visibility and host preferences are +preserved when upgrading. + +Open its options in Settings → Menubar → Widgets. Set **SSH host** to an existing +SSH alias or `user@hostname`, and optionally set a display name. For example: +`user@server.example.com`, **My server**. These are personal settings. The default menubar statistic is CPU utilization. Options include load average, memory used percent/bytes or available bytes, filesystem used percent/free bytes, @@ -49,8 +53,8 @@ dashboard while its header and footer stay fixed. - OpenSSH and Python 3 on the desktop. GNU `df` provides local filesystem stats; `ip` provides optional addresses/default-route discovery on the server. - Working noninteractive SSH key/agent authentication. First connect in a - terminal, for example `ssh john@10.10.0.7`, to verify its host key. Unknown or - changed keys are rejected; the widget never accepts them automatically. + terminal, for example `ssh user@server.example.com`, to verify its host key. + Unknown or changed keys are rejected; the widget never accepts them automatically. - Configure ports, identities and jump hosts in `~/.ssh/config`. The host field accepts a destination, not SSH options or a shell command. diff --git a/image/cybexos-desktop.spec b/image/cybexos-desktop.spec index 87c22978..45aebeee 100644 --- a/image/cybexos-desktop.spec +++ b/image/cybexos-desktop.spec @@ -18,7 +18,7 @@ Obsoletes: fedora-config-desktop < %{epoch}:%{version}-%{release} # filesystem separately. Avoid spending minutes recompressing user toolchains. %global _binary_payload w3.zstdio %global _binary_filedigest_algorithm 8 -Requires: bash coreutils util-linux systemd python3 ansible-core gnupg2 +Requires: bash coreutils util-linux systemd python3 ansible-core gnupg2 openssh-clients Requires: sddm sddm-wayland-generic systemd-pam gnome-keyring-pam Requires: hyprland hyprland-guiutils quickshell hypridle hyprlock hyprsunset Requires: xdg-desktop-portal-hyprland xdg-desktop-portal-gtk xdg-utils diff --git a/image/test_desktop_payload.py b/image/test_desktop_payload.py index 15492aad..7d515d7d 100644 --- a/image/test_desktop_payload.py +++ b/image/test_desktop_payload.py @@ -46,7 +46,7 @@ def test_portable_defaults_and_boot_assets_share_workstation_sources(self): defaults = json.loads((vendor / "essential-seed/.config/cybexos/shell.json").read_text()) self.assertEqual(defaults, contract["shell"]) # Exercise first-login seeding followed by the real shell merger: - # an unversioned seed must not revive a historical font default. + # an unversioned seed keeps current defaults and no personal host. (vendor / "bin").mkdir() (vendor / "runtime").mkdir() home = payload / "installed-user" @@ -57,11 +57,16 @@ def test_portable_defaults_and_boot_assets_share_workstation_sources(self): "node", "-e", "const fs = require('fs'), H = require(process.argv[1]); " "const s = H.merge(JSON.parse(fs.readFileSync(process.argv[2], 'utf8'))); " - "console.log(H.FONT_CHOICES.find(f => f.id === s.font).family);", + "console.log(JSON.stringify({font: H.FONT_CHOICES.find(f => f.id === s.font).family, " + "remote: s.mods.right.find(m => m.id === 'remote'), options: s.modOpts.remote}));", str(ROOT / "roles/desktop/files/quickshell/Common/SettingsHelpers.js"), str(preferences), ], text=True, capture_output=True, check=True) - self.assertEqual(result.stdout.strip(), "JetBrainsMono Nerd Font") + settings = json.loads(result.stdout) + self.assertEqual(settings["font"], "JetBrainsMono Nerd Font") + self.assertTrue(settings["remote"]["on"]) + self.assertEqual(settings["options"]["host"], "") + self.assertEqual(settings["options"]["label"], "") wallpaper = vendor / "essential-seed/Pictures/Wallpapers" / defaults["wall"] self.assertEqual(wallpaper.read_bytes(), (ROOT / "assets/wallpapers" / defaults["wall"]).read_bytes()) self.assertTrue(app.is_file()) diff --git a/roles/base/defaults/main.yml b/roles/base/defaults/main.yml index 53eb5e34..c7d50eee 100644 --- a/roles/base/defaults/main.yml +++ b/roles/base/defaults/main.yml @@ -22,6 +22,8 @@ base_required_packages: - gvfs-fuse - mokutil - NetworkManager +# Remote Server's SSH transport is available on every installation. +- openssh-clients - pipewire - pipewire-alsa - pipewire-pulseaudio diff --git a/roles/desktop/files/quickshell/Common/Settings.qml b/roles/desktop/files/quickshell/Common/Settings.qml index a07b6d12..9240fc62 100644 --- a/roles/desktop/files/quickshell/Common/Settings.qml +++ b/roles/desktop/files/quickshell/Common/Settings.qml @@ -961,7 +961,7 @@ Singleton { } // Power saver takes the compositor's polish too: cybexos_power_saver() in - // looknfeel.lua drops blur to one pass and turns animations off, and + // looknfeel.lua uses one blur pass and turns animations off, and // holds that across a config reload. It is sent on startup as well, which // also lifts a saver left behind by a shell that exited meanwhile. property bool dispatchedPowerSaver: false diff --git a/roles/desktop/files/quickshell/Common/SettingsHelpers.js b/roles/desktop/files/quickshell/Common/SettingsHelpers.js index 54017ba6..22d6a0cc 100644 --- a/roles/desktop/files/quickshell/Common/SettingsHelpers.js +++ b/roles/desktop/files/quickshell/Common/SettingsHelpers.js @@ -217,7 +217,7 @@ function defaultMods() { mod("notes", true)], right: [ mod("modelusage", false), mod("updates", true), mod("gh", false), mod("t3", false), - mod("hermes", false), mod("remote", false), + mod("hermes", false), mod("remote", true), mod("tray", false), mod("notifications", true), mod("vol", true), mod("wifi", true), mod("bt", true), mod("batt", true), mod("control", true) ] diff --git a/roles/desktop/files/quickshell/Settings/ModuleDetailView.qml b/roles/desktop/files/quickshell/Settings/ModuleDetailView.qml index b14bd2ab..af48a57d 100644 --- a/roles/desktop/files/quickshell/Settings/ModuleDetailView.qml +++ b/roles/desktop/files/quickshell/Settings/ModuleDetailView.qml @@ -911,7 +911,7 @@ SettingsPage { SettingsTextRow { width: parent.width label: "SSH host" - placeholder: "john@10.10.0.7 or SSH alias" + placeholder: "user@hostname or SSH alias" hint: "Uses your SSH config and keys. Connect once in a terminal to verify the host key. Requires Linux and Python 3.9+." value: view.opts.host dirty: view.optDirty("host") @@ -921,7 +921,7 @@ SettingsPage { SettingsTextRow { width: parent.width label: "Display name" - placeholder: "The Beast" + placeholder: "My server" value: view.opts.label dirty: view.optDirty("label") onCommitted: text => view.setOpt("label", text) diff --git a/roles/desktop/templates/looknfeel.lua.j2 b/roles/desktop/templates/looknfeel.lua.j2 index f38725ba..fdad742c 100644 --- a/roles/desktop/templates/looknfeel.lua.j2 +++ b/roles/desktop/templates/looknfeel.lua.j2 @@ -38,13 +38,11 @@ hl.config({ -- Match Quickshell's Theme.hugCornerSize so tiled window corners continue -- the menubar's inverted edge curve at the reserved boundary. rounding = 16, - -- In glass mode every menubar and panel surface is a translucent tint over - -- whatever is behind it, and without a real blur they read as smeared - -- plastic. Three passes at this radius - -- is the point where the wallpaper stops being legible through the bar; + -- Blur separates translucent surfaces from the wallpaper. One pass keeps + -- compositing work down on high-resolution, high-refresh-rate displays; -- vibrancy stands in for the design's saturate(1.7). blur = { - enabled = true, size = 6, passes = 3, new_optimizations = true, + enabled = true, size = 6, passes = 1, new_optimizations = true, vibrancy = 0.25, noise = 0.012, }, shadow = { enabled = true, range = 4, render_power = 3, color = "rgba(1a1917ee)" }, @@ -246,7 +244,7 @@ quickshell_blur_rule = hl.layer_rule({ ignore_alpha = 0.1, }) --- Power saver trades compositor polish for battery: one blur pass and no +-- Power saver keeps blur to one pass and disables -- animations. Quickshell calls cybexos_power_saver() through `hyprctl eval` -- when it starts and whenever the power profile changes. What the saver -- replaced is read live and kept in _G, which outlives a config reload: a diff --git a/tests/quickshell/power-saver.test.cjs b/tests/quickshell/power-saver.test.cjs index ecb20283..982a9e82 100644 --- a/tests/quickshell/power-saver.test.cjs +++ b/tests/quickshell/power-saver.test.cjs @@ -1,6 +1,6 @@ // Power saver is the user's explicit request to trade polish for battery. // The shell follows it with its reduced-motion path; the compositor follows -// it with fewer blur passes and no animations. +// it with one blur pass and no animations. const test = require("node:test"); const assert = require("node:assert/strict"); const childProcess = require("node:child_process"); @@ -130,7 +130,7 @@ test("power saver drops compositor blur and animations, and restores what it rep { skip: luajit ? false : "luajit is not installed" }, () => { runLookAndFeel(` dofile(look) -assert(passes() == 3 and animations() == true, "the configured values") +assert(passes() == 1 and animations() == true, "the configured values") -- A user.lua override is what comes back afterwards. hl.config({ decoration = { blur = { passes = 2 } } }) @@ -153,7 +153,7 @@ assert(calls() == before, "the shell's startup request touches nothing when off" -- A compositor restart starts a fresh state; the shell re-sends its request. _G.__cybexos_power_saver = nil dofile(look) -assert(passes() == 3 and animations() == true) +assert(passes() == 1 and animations() == true) `); }); diff --git a/tests/quickshell/remote-server.test.cjs b/tests/quickshell/remote-server.test.cjs index 96807954..34a00e0d 100644 --- a/tests/quickshell/remote-server.test.cjs +++ b/tests/quickshell/remote-server.test.cjs @@ -5,28 +5,37 @@ const S = require("../../roles/desktop/files/quickshell/Common/SettingsHelpers.j function sample() { return { version: 1, boot: "a", uptime: 1200, cpu: 25, perCore: [20, 30], load: [1, 2, 3], - meta: { hostname: "beast", os: "Linux", defaultInterface: "eth0" }, + meta: { hostname: "server", os: "Linux", defaultInterface: "eth0" }, memory: { total: 16 * 1024 ** 3, available: 12 * 1024 ** 3, used: 4 * 1024 ** 3 }, storage: [{ mount: "/", total: 1000, free: 400, percent: 60 }], temperatures: [{ name: "CPU", celsius: 54 }], - network: [{ name: "eth0", addresses: ["10.10.0.7"], rx: 1024, tx: 2048 }, + network: [{ name: "eth0", addresses: ["192.0.2.10"], rx: 1024, tx: 2048 }, { name: "docker0", addresses: ["172.17.0.1"], rx: 999999, tx: 999999 }] }; } -test("remote widget migrates as disabled and preserves user options", () => { +test("remote widget starts enabled with an empty host and preserves user choices", () => { const old = S.defaults(); old.mods.right = old.mods.right.filter(m => m.id !== "remote"); delete old.modOpts.remote; const migrated = S.merge(old); - assert.equal(migrated.mods.right.find(m => m.id === "remote").on, false); + assert.equal(migrated.mods.right.find(m => m.id === "remote").on, true); assert.equal(migrated.modOpts.remote.host, ""); + assert.equal(migrated.modOpts.remote.label, ""); assert.equal(migrated.modOpts.remote.metric, "cpu"); - const normalized = S.normalizeModOpts({ remote: { host: " john@10.10.0.7 ", label: "The Beast", + const normalized = S.normalizeModOpts({ remote: { host: " user@192.0.2.10 ", label: "My server", metric: "diskFree", mount: "/data", interface: "eth0", pollSecs: 999 } }).remote; - assert.equal(normalized.host, "john@10.10.0.7"); + assert.equal(normalized.host, "user@192.0.2.10"); assert.equal(normalized.metric, "diskFree"); assert.equal(normalized.mount, "/data"); assert.equal(normalized.pollSecs, 60); assert.equal(S.normalizeModOpts({ remote: { metric: "bogus" } }).remote.metric, "cpu"); + + const saved = S.defaults(); + saved.v = S.VERSION; + saved.mods.right.find(m => m.id === "remote").on = false; + saved.modOpts.remote = normalized; + const restored = S.merge(saved); + assert.equal(restored.mods.right.find(m => m.id === "remote").on, false); + assert.deepEqual(restored.modOpts.remote, normalized); }); test("every menu metric produces the intended unit and missing readings stay unknown", () => { const s = sample(); diff --git a/tests/quickshell/system-theme-hyprland.test.cjs b/tests/quickshell/system-theme-hyprland.test.cjs index 7acb02a7..18677b10 100644 --- a/tests/quickshell/system-theme-hyprland.test.cjs +++ b/tests/quickshell/system-theme-hyprland.test.cjs @@ -293,7 +293,7 @@ assert(hl.get_config("decoration.blur.passes") == 1, "power saver survives a liv assert(hl.get_config("animations.enabled") == false) assert(layer_rules["quickshell-blur"].enabled == true) cybexos_power_saver(false) -assert(hl.get_config("decoration.blur.passes") == 3)`, { +assert(hl.get_config("decoration.blur.passes") == 1)`, { [THEME]: render(lightTokens()), ".config/cybexos/shell.json": '{"v": 23, "glassEnabled": true}', }); diff --git a/tests/remote-server.py b/tests/remote-server.py index 306552d7..8e42a34d 100644 --- a/tests/remote-server.py +++ b/tests/remote-server.py @@ -81,7 +81,7 @@ def test_probe_caches_slow_data_and_resets_on_new_boot(self): self.assertEqual(disks.call_count, 2) def test_ssh_destination_is_data_and_host_verification_is_required(self): - for host in ["john@10.10.0.7", "beast", "john@[2001:db8::1]"]: + for host in ["user@192.0.2.10", "server", "user@[2001:db8::1]"]: command = launcher.command(host) self.assertEqual(command[-2], host) for flag in ["BatchMode=yes", "StrictHostKeyChecking=yes", "ControlPath=none", "ClearAllForwardings=yes", "ForwardAgent=no"]: