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]