diff --git a/flake.nix b/flake.nix
index a0ba69c..4691532 100644
--- a/flake.nix
+++ b/flake.nix
@@ -148,24 +148,49 @@
-I shell/modules -I shell/services -I shell/applets -I shell/lock \
shell/shell.qml shell/modules/*.qml shell/services/*.qml \
shell/applets/*.qml shell/lock/*.qml \
- > $TMPDIR/output.txt 2>&1 || true
+ > $TMPDIR/raw.txt 2>&1 || true
# Extract unique warning messages (file:message, without line numbers)
- grep -E "^Warning:" $TMPDIR/output.txt \
+ grep -E "^Warning:" $TMPDIR/raw.txt \
| sed 's/^Warning: //' \
| sed 's/\([^:]*\):[0-9]*:[0-9]*: /\1: /' \
| sort -u > $TMPDIR/current.txt
- # Diff against known baseline - new warnings = failure
- if ! diff -u test/qmllint-baseline.txt $TMPDIR/current.txt > $TMPDIR/diff.txt 2>&1; then
- new=$(grep '^+[^+]' $TMPDIR/diff.txt || true)
- if [ -n "$new" ]; then
- echo "qmllint found new warnings not in baseline:"
- echo "$new"
- exit 1
+ # Compare against baseline
+ touch $TMPDIR/new-warnings.txt $TMPDIR/stale-warnings.txt
+ while IFS= read -r line; do
+ if ! grep -qFx "$line" test/qmllint-baseline.txt 2>/dev/null; then
+ echo "$line" >> $TMPDIR/new-warnings.txt
fi
+ done < $TMPDIR/current.txt
+
+ while IFS= read -r line; do
+ if ! grep -qFx "$line" $TMPDIR/current.txt; then
+ echo "$line" >> $TMPDIR/stale-warnings.txt
+ fi
+ done < test/qmllint-baseline.txt
+
+ # Output dir with individual files for inspection
+ mkdir -p $out
+ cp $TMPDIR/raw.txt $out/raw.txt
+ cp $TMPDIR/current.txt $out/current.txt
+ cp test/qmllint-baseline.txt $out/baseline.txt
+ cp $TMPDIR/new-warnings.txt $out/new-warnings.txt
+ cp $TMPDIR/stale-warnings.txt $out/stale-warnings.txt
+
+ failed=0
+ if [ -s $TMPDIR/new-warnings.txt ]; then
+ echo "new warnings:"
+ sed 's/^/ /' $TMPDIR/new-warnings.txt
+ failed=1
fi
- cp $TMPDIR/output.txt $out
+ if [ -s $TMPDIR/stale-warnings.txt ]; then
+ echo "stale warnings:"
+ sed 's/^/ /' $TMPDIR/stale-warnings.txt
+ failed=1
+ fi
+
+ [ "$failed" -eq 0 ] || exit 1
'';
nova-stats-clippy = (pkgs.callPackage ./nix/stats-daemon.nix { }).overrideAttrs (old: {
pname = "nova-stats-clippy";
diff --git a/shell/lock/LockSurface.qml b/shell/lock/LockSurface.qml
index 6c1620d..27f14f9 100644
--- a/shell/lock/LockSurface.qml
+++ b/shell/lock/LockSurface.qml
@@ -229,6 +229,22 @@ WlSessionLockSurface {
_keyInput.forceActiveFocus();
}
+ // Skip entrance animations on wake from sleep - lock was already visible
+ Connections {
+ target: S.SleepService
+
+ function onWokeFromSleep() {
+ if (!root.lock.locked)
+ return;
+ // Qt.callLater ensures this runs after onVisibleChanged resets
+ Qt.callLater(() => {
+ _overlay._revealed = true;
+ _clockItem._revealed = true;
+ _widgetCol._revealed = true;
+ });
+ }
+ }
+
// Sync TextInput when auth clears buffer externally (PAM submit, lock reset)
Connections {
target: root.auth
diff --git a/shell/modules/BarIcon.qml b/shell/modules/BarIcon.qml
index 34e32b4..ebad597 100644
--- a/shell/modules/BarIcon.qml
+++ b/shell/modules/BarIcon.qml
@@ -60,17 +60,17 @@ Text {
onHoveredChanged: {
root._hovered = hovered;
if (hovered && root.tooltip !== "") {
- M.FlyoutState.text = root.tooltip;
- M.FlyoutState.itemX = root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0);
- M.FlyoutState.screen = QsWindow.window?.screen ?? null;
- M.FlyoutState.accentColor = root.accentColor;
- M.FlyoutState.visible = true;
+ M.TooltipState.text = root.tooltip;
+ M.TooltipState.itemX = root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0);
+ M.TooltipState.screen = QsWindow.window?.screen ?? null;
+ M.TooltipState.accentColor = root.accentColor;
+ M.TooltipState.visible = true;
} else if (!hovered && root.tooltip !== "") {
- M.FlyoutState.visible = false;
+ M.TooltipState.visible = false;
}
}
}
onTooltipChanged: if (_hovered && tooltip !== "")
- M.FlyoutState.text = tooltip
+ M.TooltipState.text = tooltip
}
diff --git a/shell/modules/BarLabel.qml b/shell/modules/BarLabel.qml
index eba1b8d..b794001 100644
--- a/shell/modules/BarLabel.qml
+++ b/shell/modules/BarLabel.qml
@@ -31,17 +31,17 @@ Text {
onHoveredChanged: {
root._hovered = hovered;
if (hovered && root.tooltip !== "") {
- M.FlyoutState.text = root.tooltip;
- M.FlyoutState.itemX = root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0);
- M.FlyoutState.screen = QsWindow.window?.screen ?? null;
- M.FlyoutState.accentColor = root.accentColor;
- M.FlyoutState.visible = true;
+ M.TooltipState.text = root.tooltip;
+ M.TooltipState.itemX = root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0);
+ M.TooltipState.screen = QsWindow.window?.screen ?? null;
+ M.TooltipState.accentColor = root.accentColor;
+ M.TooltipState.visible = true;
} else if (!hovered && root.tooltip !== "") {
- M.FlyoutState.visible = false;
+ M.TooltipState.visible = false;
}
}
}
onTooltipChanged: if (_hovered && tooltip !== "")
- M.FlyoutState.text = tooltip
+ M.TooltipState.text = tooltip
}
diff --git a/shell/modules/BarSection.qml b/shell/modules/BarSection.qml
index bb23e7d..1082e5d 100644
--- a/shell/modules/BarSection.qml
+++ b/shell/modules/BarSection.qml
@@ -20,17 +20,17 @@ Row {
onHoveredChanged: {
root._hovered = hovered;
if (hovered && root.tooltip !== "") {
- M.FlyoutState.text = root.tooltip;
- M.FlyoutState.itemX = root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0);
- M.FlyoutState.screen = QsWindow.window?.screen ?? null;
- M.FlyoutState.accentColor = root.accentColor;
- M.FlyoutState.visible = true;
+ M.TooltipState.text = root.tooltip;
+ M.TooltipState.itemX = root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0);
+ M.TooltipState.screen = QsWindow.window?.screen ?? null;
+ M.TooltipState.accentColor = root.accentColor;
+ M.TooltipState.visible = true;
} else if (!hovered && root.tooltip !== "") {
- M.FlyoutState.visible = false;
+ M.TooltipState.visible = false;
}
}
}
onTooltipChanged: if (_hovered && tooltip !== "")
- M.FlyoutState.text = tooltip
+ M.TooltipState.text = tooltip
}
diff --git a/shell/modules/PowerModule.qml b/shell/modules/PowerModule.qml
index f73c114..6321b2b 100644
--- a/shell/modules/PowerModule.qml
+++ b/shell/modules/PowerModule.qml
@@ -20,7 +20,7 @@ M.BarIcon {
cursorShape: Qt.PointingHandCursor
onClicked: {
menuLoader.active = !menuLoader.active;
- M.FlyoutState.visible = false;
+ M.TooltipState.visible = false;
}
}
diff --git a/shell/modules/Flyout.qml b/shell/modules/Tooltip.qml
similarity index 86%
rename from shell/modules/Flyout.qml
rename to shell/modules/Tooltip.qml
index f90016b..7424211 100644
--- a/shell/modules/Flyout.qml
+++ b/shell/modules/Tooltip.qml
@@ -13,7 +13,7 @@ PanelWindow {
color: "transparent"
property bool _winVisible: false
- property bool _shown: M.FlyoutState.visible && M.FlyoutState.screen === root.screen
+ property bool _shown: M.TooltipState.visible && M.TooltipState.screen === root.screen
on_ShownChanged: {
if (_shown) {
@@ -34,7 +34,7 @@ PanelWindow {
anchors.left: true
margins.top: 0
- margins.left: Math.max(0, Math.min(Math.round(M.FlyoutState.itemX - implicitWidth / 2), screen.width - implicitWidth))
+ margins.left: Math.max(0, Math.min(Math.round(M.TooltipState.itemX - implicitWidth / 2), screen.width - implicitWidth))
implicitWidth: label.implicitWidth + S.Theme.barPadding * 2
implicitHeight: label.implicitHeight + S.Theme.barPadding * 2
@@ -86,13 +86,13 @@ PanelWindow {
M.PopupBackground {
anchors.fill: parent
- accentColor: M.FlyoutState.accentColor
+ accentColor: M.TooltipState.accentColor
}
Text {
id: label
anchors.centerIn: parent
- text: M.FlyoutState.text.replace(/\n/g, "
")
+ text: M.TooltipState.text.replace(/\n/g, "
")
textFormat: Text.RichText
color: S.Theme.base05
font.pixelSize: S.Theme.fontSize
diff --git a/shell/modules/FlyoutState.qml b/shell/modules/TooltipState.qml
similarity index 100%
rename from shell/modules/FlyoutState.qml
rename to shell/modules/TooltipState.qml
diff --git a/shell/modules/TrayModule.qml b/shell/modules/TrayModule.qml
index 2224c9f..a8ed98f 100644
--- a/shell/modules/TrayModule.qml
+++ b/shell/modules/TrayModule.qml
@@ -59,13 +59,13 @@ RowLayout {
iconItem._hovered = hovered;
const tip = [iconItem.modelData.tooltipTitle, iconItem.modelData.tooltipDescription].filter(s => s).join("\n") || iconItem.modelData.title;
if (hovered && tip) {
- M.FlyoutState.text = tip;
- M.FlyoutState.itemX = iconItem.mapToGlobal(iconItem.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0);
- M.FlyoutState.screen = QsWindow.window?.screen ?? null;
- M.FlyoutState.accentColor = root.parent?.accentColor ?? S.Theme.base05;
- M.FlyoutState.visible = true;
+ M.TooltipState.text = tip;
+ M.TooltipState.itemX = iconItem.mapToGlobal(iconItem.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0);
+ M.TooltipState.screen = QsWindow.window?.screen ?? null;
+ M.TooltipState.accentColor = root.parent?.accentColor ?? S.Theme.base05;
+ M.TooltipState.visible = true;
} else if (!hovered) {
- M.FlyoutState.visible = false;
+ M.TooltipState.visible = false;
}
}
}
@@ -81,7 +81,7 @@ RowLayout {
if (root._activeMenu && root._activeMenu !== menuLoader)
root._activeMenu.active = false;
menuLoader.active = true;
- M.FlyoutState.visible = false;
+ M.TooltipState.visible = false;
root._activeMenu = menuLoader;
} else {
iconItem.modelData.secondaryActivate();
diff --git a/shell/modules/WorkspacesModule.qml b/shell/modules/WorkspacesModule.qml
index ed13927..6da1cfe 100644
--- a/shell/modules/WorkspacesModule.qml
+++ b/shell/modules/WorkspacesModule.qml
@@ -68,13 +68,13 @@ Row {
pill._hovered = hovered;
const name = pill.modelData.name || ("Workspace " + pill.modelData.idx);
if (hovered) {
- M.FlyoutState.text = name;
- M.FlyoutState.itemX = pill.mapToGlobal(pill.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0);
- M.FlyoutState.screen = QsWindow.window?.screen ?? null;
- M.FlyoutState.accentColor = root.parent?.accentColor ?? S.Theme.base05;
- M.FlyoutState.visible = true;
+ M.TooltipState.text = name;
+ M.TooltipState.itemX = pill.mapToGlobal(pill.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0);
+ M.TooltipState.screen = QsWindow.window?.screen ?? null;
+ M.TooltipState.accentColor = root.parent?.accentColor ?? S.Theme.base05;
+ M.TooltipState.visible = true;
} else {
- M.FlyoutState.visible = false;
+ M.TooltipState.visible = false;
}
}
}
diff --git a/shell/modules/qmldir b/shell/modules/qmldir
index 13ce5bd..d52222c 100644
--- a/shell/modules/qmldir
+++ b/shell/modules/qmldir
@@ -12,7 +12,6 @@ BluetoothModule 1.0 BluetoothModule.qml
ClockModule 1.0 ClockModule.qml
CpuModule 1.0 CpuModule.qml
DiskModule 1.0 DiskModule.qml
-Flyout 1.0 Flyout.qml
GpuModule 1.0 GpuModule.qml
HoverPanel 1.0 HoverPanel.qml
IdleInhibitorModule 1.0 IdleInhibitorModule.qml
@@ -36,11 +35,12 @@ ScreenCapture 1.0 ScreenCapture.qml
ScreenCorners 1.0 ScreenCorners.qml
TemperatureModule 1.0 TemperatureModule.qml
ThemedIcon 1.0 ThemedIcon.qml
+Tooltip 1.0 Tooltip.qml
TrayMenu 1.0 TrayMenu.qml
TrayModule 1.0 TrayModule.qml
VolumeModule 1.0 VolumeModule.qml
WeatherModule 1.0 WeatherModule.qml
WindowTitleModule 1.0 WindowTitleModule.qml
WorkspacesModule 1.0 WorkspacesModule.qml
-singleton FlyoutState 1.0 FlyoutState.qml
+singleton TooltipState 1.0 TooltipState.qml
# keep-sorted end
diff --git a/shell/services/SleepService.qml b/shell/services/SleepService.qml
new file mode 100644
index 0000000..4c84577
--- /dev/null
+++ b/shell/services/SleepService.qml
@@ -0,0 +1,22 @@
+pragma Singleton
+
+import QtQuick
+import Quickshell.Io
+
+QtObject {
+ id: root
+
+ signal wokeFromSleep
+
+ property Process _monitor: Process {
+ running: true
+ command: ["gdbus", "monitor", "--system", "--dest", "org.freedesktop.login1", "--object-path", "/org/freedesktop/login1"]
+
+ stdout: SplitParser {
+ onRead: data => {
+ if (data.indexOf("PrepareForSleep") !== -1 && data.indexOf("false") !== -1)
+ root.wokeFromSleep();
+ }
+ }
+ }
+}
diff --git a/shell/services/SystemStats.qml b/shell/services/SystemStats.qml
index 7345727..b2c67f0 100644
--- a/shell/services/SystemStats.qml
+++ b/shell/services/SystemStats.qml
@@ -122,10 +122,10 @@ QtObject {
}
}
- // One-time: per-core max freq
+ // One-time: per-core max freq (numerically sorted)
property var _maxFreqProc: Process {
running: true
- command: ["sh", "-c", "for f in /sys/devices/system/cpu/cpu[0-9]*/cpufreq/cpuinfo_max_freq; do [ -f \"$f\" ] && cat \"$f\" || echo 0; done 2>/dev/null"]
+ command: ["sh", "-c", "ls -d /sys/devices/system/cpu/cpu[0-9]* 2>/dev/null | sort -V | while read d; do f=\"$d/cpufreq/cpuinfo_max_freq\"; [ -f \"$f\" ] && cat \"$f\" || echo 0; done"]
stdout: StdioCollector {
onStreamFinished: {
root.cpuCoreMaxFreq = text.trim().split("\n").filter(l => l).map(l => parseInt(l) / 1e6);
@@ -134,16 +134,79 @@ QtObject {
}
// One-time: P/E-core topology
+ // Priority: cpu_core/cpu_atom sysfs > topology/core_type > freq-gap heuristic
property var _coreTypesProc: Process {
running: true
- command: ["sh", "-c", "for d in /sys/devices/system/cpu/cpu[0-9]*/topology/core_type; do [ -f \"$d\" ] && cat \"$d\" || echo Performance; done 2>/dev/null"]
+ command: ["sh", "-c", String.raw`
+ # Intel hybrid: /sys/devices/cpu_core/cpus and cpu_atom/cpus give CPU ranges
+ if [ -f /sys/devices/cpu_core/cpus ] && [ -f /sys/devices/cpu_atom/cpus ]; then
+ core=$(cat /sys/devices/cpu_core/cpus)
+ atom=$(cat /sys/devices/cpu_atom/cpus)
+ echo "hybrid:$core:$atom"
+ exit 0
+ fi
+ # Fallback: per-core topology/core_type
+ ls -d /sys/devices/system/cpu/cpu[0-9]* 2>/dev/null | sort -V | while read d; do
+ f="$d/topology/core_type"
+ [ -f "$f" ] && cat "$f"
+ done
+ `]
stdout: StdioCollector {
onStreamFinished: {
- root.cpuCoreTypes = text.trim().split("\n").filter(l => l).map(l => l.trim());
+ const out = text.trim();
+ if (!out)
+ return;
+ if (out.startsWith("hybrid:")) {
+ // Parse cpu_core/cpu_atom ranges into per-cpu type array
+ const parts = out.split(":");
+ const coreRange = parts[1];
+ const atomRange = parts[2];
+ function expandRange(s) {
+ const cpus = new Set();
+ for (const part of s.split(",")) {
+ if (part.includes("-")) {
+ const [a, b] = part.split("-").map(Number);
+ for (let i = a; i <= b; i++)
+ cpus.add(i);
+ } else {
+ cpus.add(Number(part));
+ }
+ }
+ return cpus;
+ }
+ const pCores = expandRange(coreRange);
+ const eCores = expandRange(atomRange);
+ const maxCpu = Math.max(...pCores, ...eCores);
+ const types = [];
+ for (let i = 0; i <= maxCpu; i++)
+ types.push(eCores.has(i) ? "Efficiency" : "Performance");
+ root.cpuCoreTypes = types;
+ } else {
+ // topology/core_type output
+ const types = out.split("\n").filter(l => l).map(l => l.trim());
+ if (types.length > 0)
+ root.cpuCoreTypes = types;
+ }
}
}
}
+ // Fallback: infer P/E from max freq gap when no sysfs topology is available
+ function _inferCoreTypesFromFreq() {
+ if (cpuCoreTypes.length > 0 || cpuCoreMaxFreq.length < 2)
+ return;
+ const freqs = cpuCoreMaxFreq.filter(f => f > 0);
+ if (!freqs.length)
+ return;
+ const maxF = Math.max(...freqs);
+ const minF = Math.min(...freqs);
+ if (maxF > 0 && minF > 0 && (maxF - minF) / maxF > 0.15) {
+ const threshold = (maxF + minF) / 2;
+ cpuCoreTypes = cpuCoreMaxFreq.map(f => f >= threshold ? "Performance" : "Efficiency");
+ }
+ }
+ onCpuCoreMaxFreqChanged: Qt.callLater(_inferCoreTypesFromFreq)
+
// Disk via df
property var _diskProc: Process {
id: diskProc
diff --git a/shell/services/qmldir b/shell/services/qmldir
index 901fd4e..98ac21f 100644
--- a/shell/services/qmldir
+++ b/shell/services/qmldir
@@ -13,6 +13,7 @@ singleton NiriIpc 1.0 NiriIpc.qml
singleton NotifService 1.0 NotifService.qml
singleton PowerProfileService 1.0 PowerProfileService.qml
singleton ScreenshotService 1.0 ScreenshotService.qml
+singleton SleepService 1.0 SleepService.qml
singleton SystemStats 1.0 SystemStats.qml
singleton Theme 1.0 Theme.qml
singleton WeatherService 1.0 WeatherService.qml
diff --git a/shell/shell.qml b/shell/shell.qml
index 01ddb67..032703f 100644
--- a/shell/shell.qml
+++ b/shell/shell.qml
@@ -19,7 +19,7 @@ ShellRoot {
screen: scope.modelData
}
- Flyout {
+ Tooltip {
screen: scope.modelData
}
diff --git a/test/qmllint-baseline.txt b/test/qmllint-baseline.txt
index 05382fc..a5112d2 100644
--- a/test/qmllint-baseline.txt
+++ b/test/qmllint-baseline.txt
@@ -37,11 +37,6 @@ shell/modules/BluetoothModule.qml: Unqualified access [unqualified]
shell/modules/ClockModule.qml: Member "screen" not found on type "QObject" [missing-property]
shell/modules/CpuModule.qml: Member "screen" not found on type "QObject" [missing-property]
shell/modules/DiskModule.qml: Member "screen" not found on type "QObject" [missing-property]
-shell/modules/Flyout.qml: Could not find property "left". [missing-property]
-shell/modules/Flyout.qml: Could not find property "top". [missing-property]
-shell/modules/Flyout.qml: Type PanelWindow is not creatable. [uncreatable-type]
-shell/modules/Flyout.qml: Type margins is used but it is not resolved [unresolved-type]
-shell/modules/Flyout.qml: unknown grouped property scope margins. [unqualified]
shell/modules/GpuModule.qml: Member "screen" not found on type "QObject" [missing-property]
shell/modules/HoverPanel.qml: Could not find property "top". [missing-property]
shell/modules/HoverPanel.qml: Type PanelWindow is not creatable. [uncreatable-type]
@@ -69,6 +64,11 @@ shell/modules/ScreenCorners.qml: Type PanelWindow is not creatable. [uncreatable
shell/modules/ScreenCorners.qml: Unqualified access [unqualified]
shell/modules/TemperatureModule.qml: Member "screen" not found on type "QObject" [missing-property]
shell/modules/ThemedIcon.qml: Unqualified access [unqualified]
+shell/modules/Tooltip.qml: Could not find property "left". [missing-property]
+shell/modules/Tooltip.qml: Could not find property "top". [missing-property]
+shell/modules/Tooltip.qml: Type PanelWindow is not creatable. [uncreatable-type]
+shell/modules/Tooltip.qml: Type margins is used but it is not resolved [unresolved-type]
+shell/modules/Tooltip.qml: unknown grouped property scope margins. [unqualified]
shell/modules/TrayMenu.qml: Unqualified access [unqualified]
shell/modules/TrayModule.qml: Member "length" not found on type "UntypedObjectModel" [missing-property]
shell/modules/TrayModule.qml: Member "screen" not found on type "QObject" [missing-property]