Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
12 changes: 8 additions & 4 deletions docs/quickshell-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 11 additions & 7 deletions docs/remote-server-widget.md
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion image/cybexos-desktop.spec
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 8 additions & 3 deletions image/test_desktop_payload.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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())
Expand Down
2 changes: 2 additions & 0 deletions roles/base/defaults/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion roles/desktop/files/quickshell/Common/Settings.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion roles/desktop/files/quickshell/Common/SettingsHelpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
]
Expand Down
4 changes: 2 additions & 2 deletions roles/desktop/files/quickshell/Settings/ModuleDetailView.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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)
Expand Down
10 changes: 4 additions & 6 deletions roles/desktop/templates/looknfeel.lua.j2
Original file line number Diff line number Diff line change
Expand Up @@ -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)" },
Expand Down Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions tests/quickshell/power-saver.test.cjs
Original file line number Diff line number Diff line change
@@ -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");
Expand Down Expand Up @@ -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 } } })

Expand All @@ -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)
`);
});

Expand Down
21 changes: 15 additions & 6 deletions tests/quickshell/remote-server.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion tests/quickshell/system-theme-hyprland.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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}',
});
Expand Down
2 changes: 1 addition & 1 deletion tests/remote-server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]:
Expand Down
Loading