Skip to content
Draft

Nexus #1397

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
989a45b
feat: layouting draft
PixelKhaos Apr 11, 2026
0438e82
fix: better layout, search/editing context
PixelKhaos Apr 12, 2026
d1134be
feat: easings, windowFactory resizing/floating toggle, expanded nav d…
PixelKhaos Apr 13, 2026
25b17dc
feat: search index
PixelKhaos Apr 13, 2026
eda60d9
feat: handle tab navigation in category
PixelKhaos Apr 13, 2026
7e9785a
chore: linting
PixelKhaos Apr 13, 2026
8d4265d
chore: linting
PixelKhaos Apr 14, 2026
7d039e8
feat: sdfs with deformation used for flyouts/popouts
PixelKhaos Apr 14, 2026
4f0b1f2
feat: flyout & popout 'dodging' overlap avoidance
PixelKhaos Apr 14, 2026
75e7c9b
fix: combine popouts, easing changes, parent category chevron animation
PixelKhaos Apr 14, 2026
c958b0f
feat: category and tab content transitions
PixelKhaos Apr 14, 2026
2766329
Merge remote-tracking branch 'upstream/main' into nexus
PixelKhaos Apr 15, 2026
165b6fa
chore: update to new config & tokens, linting
PixelKhaos Apr 15, 2026
7ac3f30
feat: add anim type shorthand + anchor anim
soramanew Apr 15, 2026
0dee7eb
refactor: use new anim type prop
soramanew Apr 15, 2026
f259c05
refactor: use new AnchorAnim component
soramanew Apr 15, 2026
c8f0528
chore: remove unused imports
soramanew Apr 15, 2026
d618c2c
feat: updates panel, folder structure for panels
PixelKhaos Apr 16, 2026
ee7074a
feat: layouting draft
PixelKhaos Apr 11, 2026
f0ca426
fix: better layout, search/editing context
PixelKhaos Apr 12, 2026
8e3110c
feat: easings, windowFactory resizing/floating toggle, expanded nav d…
PixelKhaos Apr 13, 2026
a4a7e8a
feat: search index
PixelKhaos Apr 13, 2026
909df21
feat: handle tab navigation in category
PixelKhaos Apr 13, 2026
649c797
chore: linting
PixelKhaos Apr 13, 2026
5e21ace
chore: linting
PixelKhaos Apr 14, 2026
94dc45d
feat: sdfs with deformation used for flyouts/popouts
PixelKhaos Apr 14, 2026
dcea76d
feat: flyout & popout 'dodging' overlap avoidance
PixelKhaos Apr 14, 2026
e8ad311
fix: combine popouts, easing changes, parent category chevron animation
PixelKhaos Apr 14, 2026
542dfbd
feat: category and tab content transitions
PixelKhaos Apr 14, 2026
3d50a1f
chore: update to new config & tokens, linting
PixelKhaos Apr 15, 2026
84a8aac
feat: updates panel, folder structure for panels
PixelKhaos Apr 16, 2026
eef1842
Merge branch 'main' into nexus
PixelKhaos Apr 16, 2026
8990e40
Merge remote-tracking branch 'origin/nexus' into nexus
PixelKhaos Apr 16, 2026
952a9bf
fix: account for blob colour alpha
soramanew Apr 17, 2026
c0c61cc
fix: nexus blob overlapping when transparent
soramanew Apr 17, 2026
10c1c50
fix: remove content wrappers
soramanew Apr 17, 2026
15c2870
fix: clamp initial nexus height at 1000
soramanew Apr 17, 2026
22385ff
Merge branch 'main' into nexus
soramanew Apr 17, 2026
4a7c897
fix: use state layer signals
soramanew Apr 17, 2026
4183522
feat: improve window controls
soramanew Apr 17, 2026
6290722
chore: format
soramanew Apr 17, 2026
230979d
chore: format imports + no relative imports
soramanew Apr 17, 2026
06dea2b
Merge branch 'main' into nexus
soramanew Apr 17, 2026
fd89331
Merge branch 'nexus' of https://github.com/PixelKhaos/shell into nexus
PixelKhaos Apr 17, 2026
d540c50
feat: power/idle panel & management
PixelKhaos Apr 18, 2026
7ad8035
Merge remote-tracking branch 'upstream/main' into nexus
PixelKhaos Apr 18, 2026
c1f6037
chore: formatting
PixelKhaos Apr 18, 2026
7de442a
fix: use listview for idle & threshold lists, added animation
PixelKhaos Apr 18, 2026
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
250 changes: 244 additions & 6 deletions modules/BatteryMonitor.qml
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,218 @@ import Quickshell
import Quickshell.Services.UPower
import Caelestia
import Caelestia.Config
import qs.services

Scope {
id: root

readonly property list<var> warnLevels: [...GlobalConfig.general.battery.warnLevels].sort((a, b) => b.level - a.level)

readonly property var pm: GlobalConfig.general.battery.powerManagement
readonly property bool pmEnabled: pm && pm.enabled === true
readonly property list<var> powerThresholds: {
const t = (pm && pm.thresholds) || [];
return [...t].sort((a, b) => (b.level ?? 0) - (a.level ?? 0));
}

property var originalRefreshRates: ({})
property int currentThresholdIndex: -1
property bool settingsModified: false
property bool initialized: false

function applyVisualEffects(settings): void {
const options = {};
if (settings.disableAnimations === "disable")
options["animations:enabled"] = 0;
else if (settings.disableAnimations === "enable")
options["animations:enabled"] = 1;
if (settings.disableBlur === "disable")
options["decoration:blur:enabled"] = 0;
else if (settings.disableBlur === "enable")
options["decoration:blur:enabled"] = 1;
if (settings.disableRounding === "disable")
options["decoration:rounding"] = 0;
else if (settings.disableRounding === "enable")
options["decoration:rounding"] = GlobalConfig.appearance.rounding.normal;
if (settings.disableShadows === "disable")
options["decoration:shadow:enabled"] = 0;
else if (settings.disableShadows === "enable")
options["decoration:shadow:enabled"] = 1;
if (Object.keys(options).length > 0)
Hypr.extras.applyOptions(options); // qmllint disable missing-property
}

function applyRefreshRate(rate): void {
if (rate === "restore") {
restoreRefreshRates();
return;
}
const targetRate = rate === "auto" ? getLowestRefreshRate() : rate;
const monitors = Object.values(Hypr.monitors.values || Hypr.monitors);
for (const monitor of monitors) {
const data = monitor.lastIpcObject;
if (data)
Hypr.extras.message(`keyword monitor ${data.name},${data.width}x${data.height}@${targetRate},${data.x}x${data.y},${data.scale}`); // qmllint disable missing-property
}
}

function getLowestRefreshRate(): real {
const monitors = Object.values(Hypr.monitors.values || Hypr.monitors);
let lowestRate = 60;
for (const monitor of monitors) {
const data = monitor.lastIpcObject;
if (data && data.availableModes && data.availableModes.length > 0) {
const rates = [];
for (const mode of data.availableModes) {
const m = mode.match(/@(\d+(?:\.\d+)?)Hz/);
if (m) {
const r = Math.round(parseFloat(m[1]));
if (!rates.includes(r))
rates.push(r);
}
}
rates.sort((a, b) => a - b);
if (rates.length > 0)
lowestRate = Math.min(lowestRate, rates[0]);
}
}
return lowestRate;
}

function saveOriginalSettings(): void {
const monitors = Object.values(Hypr.monitors.values || Hypr.monitors);
const next = {};
for (const monitor of monitors) {
const data = monitor.lastIpcObject;
if (data)
next[data.name] = data.refreshRate;
}
root.originalRefreshRates = next;
}

function restoreRefreshRates(): void {
const monitors = Object.values(Hypr.monitors.values || Hypr.monitors);
for (const monitor of monitors) {
const data = monitor.lastIpcObject;
if (data && root.originalRefreshRates[data.name]) {
const orig = root.originalRefreshRates[data.name];
Hypr.extras.message(`keyword monitor ${data.name},${data.width}x${data.height}@${orig},${data.x}x${data.y},${data.scale}`); // qmllint disable missing-property
}
}
}

function setPowerProfile(name): void {
const map = {
"power-saver": PowerProfile.PowerSaver,
"balanced": PowerProfile.Balanced,
"performance": PowerProfile.Performance
};
if (map[name] !== undefined)
PowerProfiles.profile = map[name];
}

function handleUnpluggedState(): void {
const cfg = (root.pm && root.pm.onUnplugged) || {};
const hasActions = (cfg.setPowerProfile ?? "") !== "" || (cfg.setRefreshRate ?? "") !== "" || (cfg.disableAnimations ?? "") !== "" || (cfg.disableBlur ?? "") !== "" || (cfg.disableRounding ?? "") !== "" || (cfg.disableShadows ?? "") !== "";
if (hasActions) {
if (!root.settingsModified)
root.saveOriginalSettings();
if ((cfg.setPowerProfile ?? "") !== "")
root.setPowerProfile(cfg.setPowerProfile);
root.applyVisualEffects(cfg);
if ((cfg.setRefreshRate ?? "") !== "")
root.applyRefreshRate(cfg.setRefreshRate);
root.settingsModified = true;
}
if (cfg.evaluateThresholds !== false)
root.evaluateThresholds();
}

function handleChargingState(): void {
const cfg = (root.pm && root.pm.onCharging) || {};
if (cfg.setPowerProfile === "restore")
PowerProfiles.profile = PowerProfile.Balanced;
else if ((cfg.setPowerProfile ?? "") !== "")
root.setPowerProfile(cfg.setPowerProfile);
if ((cfg.setRefreshRate ?? "") !== "" && cfg.setRefreshRate !== "unchanged")
root.applyRefreshRate(cfg.setRefreshRate);
root.applyVisualEffects(cfg);
root.settingsModified = false;
root.currentThresholdIndex = -1;
}

function evaluateThresholds(): void {
if (!UPower.onBattery || !root.pmEnabled)
return;
const p = UPower.displayDevice.percentage * 100;
let target = -1;
for (let i = 0; i < root.powerThresholds.length; i++) {
if (p <= (root.powerThresholds[i].level ?? 0)) {
target = i;
break;
}
}
if (target !== root.currentThresholdIndex) {
root.currentThresholdIndex = target;
if (target >= 0)
root.applyThreshold(root.powerThresholds[target]);
}
}

function applyThreshold(threshold): void {
if (!root.settingsModified)
root.saveOriginalSettings();
if ((threshold.setPowerProfile ?? "") !== "")
root.setPowerProfile(threshold.setPowerProfile);
root.applyVisualEffects(threshold);
if ((threshold.setRefreshRate ?? "") !== "")
root.applyRefreshRate(threshold.setRefreshRate);
root.settingsModified = true;

if (GlobalConfig.utilities.toasts.lowPowerModeChanged && root.initialized) {
const actions = [];
if ((threshold.setPowerProfile ?? "") !== "")
actions.push(qsTr("profile: ") + threshold.setPowerProfile);
if ((threshold.setRefreshRate ?? "") !== "")
actions.push(threshold.setRefreshRate === "auto" ? qsTr("lowest Hz") : threshold.setRefreshRate + "Hz");
if (threshold.disableAnimations === "disable")
actions.push(qsTr("no animations"));
if (threshold.disableBlur === "disable")
actions.push(qsTr("no blur"));
Toaster.toast(qsTr("Battery saving active"), qsTr("Applied: ") + actions.join(", "), "battery_saver");
}
}

Component.onCompleted: initTimer.start()

Timer {
id: initTimer

interval: 1000
onTriggered: root.initialized = true
}

Timer {
id: hibernateTimer

interval: 5000
onTriggered: Quickshell.execDetached(["systemctl", "hibernate"])
}

Connections {
function onOnBatteryChanged(): void {
if (UPower.onBattery) {
if (GlobalConfig.utilities.toasts.chargingChanged)
if (GlobalConfig.utilities.toasts.chargingChanged && root.initialized)
Toaster.toast(qsTr("Charger unplugged"), qsTr("Battery is discharging"), "power_off");
if (root.pmEnabled)
root.handleUnpluggedState();
} else {
if (GlobalConfig.utilities.toasts.chargingChanged)
if (GlobalConfig.utilities.toasts.chargingChanged && root.initialized)
Toaster.toast(qsTr("Charger plugged in"), qsTr("Battery is charging"), "power");
for (const level of root.warnLevels)
level.warned = false;
if (root.pmEnabled)
root.handleChargingState();
}
}

Expand All @@ -42,15 +238,57 @@ Scope {
Toaster.toast(qsTr("Hibernating in 5 seconds"), qsTr("Hibernating to prevent data loss"), "battery_android_alert", Toast.Error);
hibernateTimer.start();
}

if (root.pmEnabled)
root.evaluateThresholds();
}

target: UPower.displayDevice
}

Timer {
id: hibernateTimer
Connections {
function onProfileChanged(): void {
if (!root.pmEnabled)
return;

interval: 5000
onTriggered: Quickshell.execDetached(["systemctl", "hibernate"])
const profileBehaviors = (root.pm && root.pm.profileBehaviors) || {};
let behavior = null;
let profileName = "";
if (PowerProfiles.profile === PowerProfile.PowerSaver) {
behavior = profileBehaviors.powerSaver;
profileName = qsTr("Power Saver");
} else if (PowerProfiles.profile === PowerProfile.Balanced) {
behavior = profileBehaviors.balanced;
profileName = qsTr("Balanced");
} else if (PowerProfiles.profile === PowerProfile.Performance) {
behavior = profileBehaviors.performance;
profileName = qsTr("Performance");
}
if (!behavior)
return;

if (behavior.setRefreshRate && behavior.setRefreshRate !== "" && behavior.setRefreshRate !== "unchanged")
root.applyRefreshRate(behavior.setRefreshRate);
root.applyVisualEffects(behavior);

const hasSettings = (behavior.disableAnimations ?? "") !== "" || (behavior.disableBlur ?? "") !== "" || (behavior.disableRounding ?? "") !== "" || (behavior.disableShadows ?? "") !== "" || (behavior.setRefreshRate && behavior.setRefreshRate !== "" && behavior.setRefreshRate !== "restore");
if (GlobalConfig.utilities.toasts.lowPowerModeChanged && hasSettings && root.initialized) {
const actions = [];
if (behavior.setRefreshRate && behavior.setRefreshRate !== "")
actions.push(behavior.setRefreshRate === "auto" ? qsTr("lowest Hz") : behavior.setRefreshRate + "Hz");
if (behavior.disableAnimations === "disable")
actions.push(qsTr("no animations"));
else if (behavior.disableAnimations === "enable")
actions.push(qsTr("animations on"));
if (behavior.disableBlur === "disable")
actions.push(qsTr("no blur"));
else if (behavior.disableBlur === "enable")
actions.push(qsTr("blur on"));
if (actions.length > 0)
Toaster.toast(profileName + qsTr(" profile"), qsTr("Applied: ") + actions.join(", "), "battery_saver");
}
}

target: PowerProfiles
}
}
17 changes: 17 additions & 0 deletions modules/Shortcuts.qml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Caelestia
import qs.components.misc
import qs.services
import qs.modules.controlcenter
import qs.modules.nexus

Scope {
id: root
Expand All @@ -20,6 +21,14 @@ Scope {
onPressed: WindowFactory.create()
}

// qmllint disable unresolved-type
CustomShortcut {
// qmllint enable unresolved-type
name: "nexus"
description: "Open Nexus settings"
onPressed: NexusWindowFactory.create()
}

// qmllint disable unresolved-type
CustomShortcut {
// qmllint enable unresolved-type
Expand Down Expand Up @@ -136,6 +145,14 @@ Scope {
target: "controlCenter"
}

IpcHandler {
function open(): void {
NexusWindowFactory.create();
}

target: "nexus"
}

IpcHandler {
function info(title: string, message: string, icon: string): void {
Toaster.toast(title, message, icon, Toast.Info);
Expand Down
4 changes: 4 additions & 0 deletions modules/drawers/ContentWindow.qml
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ StyledWindow {
shadowColor: Qt.alpha(Colours.palette.m3shadow, Math.max(0, root.shadowOpacity))
}

Behavior on opacity {
Anim {}
}

BlobGroup {
id: blobGroup

Expand Down
Loading