diff --git a/dots/.config/hypr/hyprland/execs.conf b/dots/.config/hypr/hyprland/execs.conf index 252e3afa8..e4f33f6aa 100644 --- a/dots/.config/hypr/hyprland/execs.conf +++ b/dots/.config/hypr/hyprland/execs.conf @@ -23,3 +23,5 @@ exec-once = wl-paste --type image --watch bash -c 'cliphist store && qs -c $qsCo # Cursor exec-once = hyprctl setcursor Bibata-Modern-Classic 24 +# Fix dock pinned apps not launching properly (https://github.com/end-4/dots-hyprland/issues/2200) +exec-once = sleep 3.5 && hyprctl reload && sleep 0.5 && touch ~/.config/quickshell/ii/shell.qml diff --git a/dots/.config/quickshell/ii/modules/common/Config.qml b/dots/.config/quickshell/ii/modules/common/Config.qml index d064d2533..363c4c906 100644 --- a/dots/.config/quickshell/ii/modules/common/Config.qml +++ b/dots/.config/quickshell/ii/modules/common/Config.qml @@ -380,6 +380,7 @@ Singleton { property JsonObject overlay: JsonObject { property bool openingZoomAnimation: true property bool darkenScreen: true + property real clickthroughOpacity: 0.7 } property JsonObject overview: JsonObject { diff --git a/dots/.config/quickshell/ii/modules/common/Persistent.qml b/dots/.config/quickshell/ii/modules/common/Persistent.qml index f2de212c9..5848b1683 100644 --- a/dots/.config/quickshell/ii/modules/common/Persistent.qml +++ b/dots/.config/quickshell/ii/modules/common/Persistent.qml @@ -85,7 +85,7 @@ Singleton { property bool pinned: false property bool clickthrough: true property real x: 835 - property real y: 490 + property real y: 483 } property JsonObject recorder: JsonObject { property bool pinned: false @@ -104,7 +104,14 @@ Singleton { property bool pinned: false property bool clickthrough: false property real x: 80 - property real y: 250 + property real y: 280 + property int tabIndex: 0 + } + property JsonObject fpsLimiter: JsonObject { + property bool pinned: false + property bool clickthrough: false + property real x: 1576 + property real y: 630 } } diff --git a/dots/.config/quickshell/ii/modules/common/widgets/GroupButton.qml b/dots/.config/quickshell/ii/modules/common/widgets/GroupButton.qml index de9ce5603..080439613 100644 --- a/dots/.config/quickshell/ii/modules/common/widgets/GroupButton.qml +++ b/dots/.config/quickshell/ii/modules/common/widgets/GroupButton.qml @@ -117,7 +117,7 @@ Button { }; } - + property bool tabbedTo: root.focus && (focusReason === Qt.TabFocusReason || focusReason === Qt.BacktabFocusReason) background: Rectangle { id: buttonBackground topLeftRadius: root.leftRadius @@ -130,6 +130,9 @@ Button { Behavior on color { animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this) } + + border.width: root.tabbedTo ? 2 : 0 + border.color: Appearance.colors.colSecondary } contentItem: StyledText { diff --git a/dots/.config/quickshell/ii/modules/common/widgets/IconToolbarButton.qml b/dots/.config/quickshell/ii/modules/common/widgets/IconToolbarButton.qml index df87dcb0e..6e2fd4166 100644 --- a/dots/.config/quickshell/ii/modules/common/widgets/IconToolbarButton.qml +++ b/dots/.config/quickshell/ii/modules/common/widgets/IconToolbarButton.qml @@ -18,5 +18,6 @@ ToolbarButton { iconSize: 22 text: iconBtn.text color: iconBtn.colText + animateChange: true } } diff --git a/dots/.config/quickshell/ii/modules/overlay/Overlay.qml b/dots/.config/quickshell/ii/modules/overlay/Overlay.qml index 5054b9d1c..71ba510c6 100644 --- a/dots/.config/quickshell/ii/modules/overlay/Overlay.qml +++ b/dots/.config/quickshell/ii/modules/overlay/Overlay.qml @@ -25,7 +25,7 @@ Scope { exclusionMode: ExclusionMode.Ignore WlrLayershell.namespace: "quickshell:overlay" WlrLayershell.layer: WlrLayer.Overlay - WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand + WlrLayershell.keyboardFocus: GlobalStates.overlayOpen ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None visible: true color: "transparent" @@ -43,6 +43,30 @@ Scope { right: true } + HyprlandFocusGrab { + id: grab + windows: [overlayWindow] + active: false + onCleared: () => { + if (!active) GlobalStates.overlayOpen = false; + } + } + + Connections { + target: GlobalStates + function onOverlayOpenChanged() { + delayedGrabTimer.restart(); + } + } + + Timer { + id: delayedGrabTimer + interval: Appearance.animation.elementMoveFast.duration + onTriggered: { + grab.active = GlobalStates.overlayOpen; + } + } + OverlayContent { id: overlayContent anchors.fill: parent diff --git a/dots/.config/quickshell/ii/modules/overlay/OverlayContent.qml b/dots/.config/quickshell/ii/modules/overlay/OverlayContent.qml index 838267b80..546962185 100644 --- a/dots/.config/quickshell/ii/modules/overlay/OverlayContent.qml +++ b/dots/.config/quickshell/ii/modules/overlay/OverlayContent.qml @@ -12,6 +12,7 @@ import qs.modules.overlay.crosshair Item { id: root + focus: true readonly property bool usePasswordChars: !PolkitService.flow?.responseVisible ?? true Keys.onPressed: (event) => { // Esc to close diff --git a/dots/.config/quickshell/ii/modules/overlay/OverlayContext.qml b/dots/.config/quickshell/ii/modules/overlay/OverlayContext.qml index c13b1933c..b228deda6 100644 --- a/dots/.config/quickshell/ii/modules/overlay/OverlayContext.qml +++ b/dots/.config/quickshell/ii/modules/overlay/OverlayContext.qml @@ -9,6 +9,7 @@ Singleton { { identifier: "recorder", materialSymbol: "screen_record" }, { identifier: "volumeMixer", materialSymbol: "volume_up" }, { identifier: "crosshair", materialSymbol: "point_scan" }, + { identifier: "fpsLimiter", materialSymbol: "animation" }, { identifier: "resources", materialSymbol: "browse_activity" } ] diff --git a/dots/.config/quickshell/ii/modules/overlay/OverlayWidgetDelegateChooser.qml b/dots/.config/quickshell/ii/modules/overlay/OverlayWidgetDelegateChooser.qml index 90f85a7fd..fc75d1455 100644 --- a/dots/.config/quickshell/ii/modules/overlay/OverlayWidgetDelegateChooser.qml +++ b/dots/.config/quickshell/ii/modules/overlay/OverlayWidgetDelegateChooser.qml @@ -8,6 +8,7 @@ import Quickshell import Quickshell.Bluetooth import qs.modules.overlay.crosshair import qs.modules.overlay.volumeMixer +import qs.modules.overlay.fpsLimiter import qs.modules.overlay.recorder import qs.modules.overlay.resources @@ -17,6 +18,7 @@ DelegateChooser { DelegateChoice { roleValue: "crosshair"; Crosshair {} } DelegateChoice { roleValue: "volumeMixer"; VolumeMixer {} } + DelegateChoice { roleValue: "fpsLimiter"; FpsLimiter {} } DelegateChoice { roleValue: "recorder"; Recorder {} } DelegateChoice { roleValue: "resources"; Resources {} } } diff --git a/dots/.config/quickshell/ii/modules/overlay/StyledOverlayWidget.qml b/dots/.config/quickshell/ii/modules/overlay/StyledOverlayWidget.qml index 5e6a0ce3f..f6c21199b 100644 --- a/dots/.config/quickshell/ii/modules/overlay/StyledOverlayWidget.qml +++ b/dots/.config/quickshell/ii/modules/overlay/StyledOverlayWidget.qml @@ -21,6 +21,9 @@ AbstractOverlayWidget { id: root required property Item contentItem + property bool fancyBorders: true + property bool showCenterButton: false + property bool showClickabilityButton: true required property var modelData readonly property string identifier: modelData.identifier @@ -29,6 +32,8 @@ AbstractOverlayWidget { property var persistentStateEntry: Persistent.states.overlay[identifier] property real radius: Appearance.rounding.windowRounding property real minWidth: 250 + property real padding: 6 + property real contentRadius: radius - padding draggable: GlobalStates.overlayOpen x: Math.round(persistentStateEntry.x) // Round or it'll be blurry @@ -38,9 +43,10 @@ AbstractOverlayWidget { drag { minimumX: 0 minimumY: 0 - maximumX: root.parent.width - root.width - maximumY: root.parent.height - root.height + maximumX: root.parent?.width - root.width + maximumY: root.parent?.height - root.height } + opacity: (GlobalStates.overlayOpen || !clickthrough) ? 1.0 : Config.options.overlay.clickthroughOpacity // Guarded states & registration funcs property bool open: Persistent.states.overlay.open @@ -83,7 +89,7 @@ AbstractOverlayWidget { function center() { const targetX = (root.parent.width - contentColumn.width) / 2 - const targetY = (root.parent.height - contentItem.height) / 2 - titleBar.implicitHeight + const targetY = (root.parent.height - contentItem.height) / 2 - titleBar.implicitHeight + border.border.width root.x = targetX root.y = targetY root.savePosition(targetX, targetY) @@ -96,11 +102,15 @@ AbstractOverlayWidget { Rectangle { id: border anchors.fill: parent - color: "transparent" + color: (root.fancyBorders && GlobalStates.overlayOpen) ? Appearance.colors.colLayer1 : "transparent" radius: root.radius border.color: ColorUtils.transparentize(Appearance.colors.colOutlineVariant, GlobalStates.overlayOpen ? 0 : 1) border.width: 1 + Behavior on color { + animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this) + } + layer.enabled: GlobalStates.overlayOpen layer.effect: OpacityMask { maskSource: Rectangle { @@ -110,37 +120,34 @@ AbstractOverlayWidget { } } - Column { + ColumnLayout { id: contentColumn - z: -1 + z: root.fancyBorders ? 0 : -1 anchors.fill: parent + spacing: 0 // Title bar Rectangle { id: titleBar opacity: GlobalStates.overlayOpen ? 1 : 0 - anchors { - left: parent.left - right: parent.right - } - property real padding: 2 - implicitWidth: titleBarRow.implicitWidth + padding * 2 - implicitHeight: titleBarRow.implicitHeight + padding * 2 - color: Appearance.m3colors.m3surfaceContainer - border.color: Appearance.colors.colOutlineVariant - border.width: 1 + Layout.fillWidth: true + implicitWidth: titleBarRow.implicitWidth + root.padding * 2 + implicitHeight: titleBarRow.implicitHeight + root.padding * 2 + color: root.fancyBorders ? "transparent" : Appearance.colors.colLayer1 + // border.color: Appearance.colors.colOutlineVariant + // border.width: 1 RowLayout { id: titleBarRow anchors { fill: parent - margins: titleBar.padding - leftMargin: titleBar.padding + 8 + margins: root.padding } - spacing: 0 + spacing: 2 MaterialSymbol { text: root.materialSymbol + Layout.leftMargin: 6 iconSize: 20 Layout.alignment: Qt.AlignVCenter Layout.rightMargin: 4 @@ -153,6 +160,7 @@ AbstractOverlayWidget { } TitlebarButton { + visible: root.showCenterButton materialSymbol: "recenter" onClicked: root.center() StyledToolTip { @@ -161,6 +169,7 @@ AbstractOverlayWidget { } TitlebarButton { + visible: (root.pinned && root.showClickabilityButton) materialSymbol: "mouse" toggled: !root.clickthrough onClicked: root.toggleClickthrough() @@ -191,7 +200,11 @@ AbstractOverlayWidget { // Content Item { id: contentContainer - anchors.horizontalCenter: parent.horizontalCenter + Layout.fillWidth: true + Layout.fillHeight: true + Layout.margins: root.fancyBorders ? root.padding : 0 + Layout.topMargin: -border.border.width // Border of a rectangle is drawn inside its bounds, so we do this to make the gap not too big + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter implicitWidth: root.contentItem.implicitWidth implicitHeight: root.contentItem.implicitHeight children: [root.contentItem] diff --git a/dots/.config/quickshell/ii/modules/overlay/crosshair/Crosshair.qml b/dots/.config/quickshell/ii/modules/overlay/crosshair/Crosshair.qml index abb1cbf26..4a92aa43e 100644 --- a/dots/.config/quickshell/ii/modules/overlay/crosshair/Crosshair.qml +++ b/dots/.config/quickshell/ii/modules/overlay/crosshair/Crosshair.qml @@ -1,9 +1,18 @@ import QtQuick +import QtQuick.Layouts import Quickshell import qs.modules.common import qs.modules.overlay StyledOverlayWidget { id: root - contentItem: CrosshairContent {} + fancyBorders: false // Crosshair should be see-through + showCenterButton: true + opacity: 1 // The crosshair itself already has transparency if configured + showClickabilityButton: false + clickthrough: true + + contentItem: CrosshairContent { + anchors.centerIn: parent + } } diff --git a/dots/.config/quickshell/ii/modules/overlay/fpsLimiter/FpsLimiter.qml b/dots/.config/quickshell/ii/modules/overlay/fpsLimiter/FpsLimiter.qml new file mode 100644 index 000000000..7628d58be --- /dev/null +++ b/dots/.config/quickshell/ii/modules/overlay/fpsLimiter/FpsLimiter.qml @@ -0,0 +1,12 @@ +import QtQuick +import Quickshell +import qs.modules.common +import qs.modules.overlay + +StyledOverlayWidget { + id: root + title: "MangoHud FPS" + contentItem: FpsLimiterContent { + radius: root.contentRadius + } +} diff --git a/dots/.config/quickshell/ii/modules/overlay/fpsLimiter/FpsLimiterContent.qml b/dots/.config/quickshell/ii/modules/overlay/fpsLimiter/FpsLimiterContent.qml new file mode 100644 index 000000000..a67bf40c1 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/overlay/fpsLimiter/FpsLimiterContent.qml @@ -0,0 +1,97 @@ +import qs.services +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell +import Quickshell.Io +import qs.modules.common +import qs.modules.common.widgets + +Rectangle { + id: root + + enum State { Normal, Success, Error } + + anchors.fill: parent + property real padding: 16 + property var currentState: FpsLimiterContent.State.Normal + color: Appearance.m3colors.m3surfaceContainer + implicitWidth: content.implicitWidth + (padding * 2) + implicitHeight: content.implicitHeight + (padding * 2) + + Timer { + id: iconResetTimer + interval: 1000 + onTriggered: { + root.currentState = FpsLimiterContent.State.Normal; + } + } + + function applyLimit() { + var fpsValue = parseInt(fpsField.text); + if (isNaN(fpsValue) || fpsValue < 0) { + root.currentState = FpsLimiterContent.State.Error; + iconResetTimer.restart(); + fpsField.text = ""; + return; + } + + var cfgPaths = [ + "~/.config/MangoHud/MangoHud.conf", + ]; // MangoHud config files + + var updateCommands = cfgPaths.map(path => { + return "if grep -q '^fps_limit=' " + path + "; " + + "then sed -i 's/^fps_limit=.*/fps_limit=" + fpsValue + "/' " + path + "; " + + "else echo 'fps_limit=" + fpsValue + "' >> " + path + "; fi"; + }).join("; "); + + var cmd = updateCommands + "; pkill -SIGUSR2 mangohud"; + + fpsSetter.command = ["bash", "-c", cmd]; + fpsSetter.startDetached(); + + root.currentState = FpsLimiterContent.State.Success; + iconResetTimer.restart(); + + // Clear the field after applying + fpsField.text = ""; + } + + Process { + id: fpsSetter + } + + RowLayout { + id: content + anchors.centerIn: parent + spacing: 4 + + ToolbarTextField { + id: fpsField + Layout.fillWidth: true + Layout.preferredWidth: 200 + placeholderText: root.currentState === FpsLimiterContent.State.Error ? Translation.tr("Enter a valid number") : Translation.tr("Set FPS limit") + inputMethodHints: Qt.ImhDigitsOnly + focus: true + + onAccepted: { + root.applyLimit(); + } + } + + IconToolbarButton { + id: applyButton + text: switch (root.currentState) { + case FpsLimiterContent.State.Error: return "close"; + case FpsLimiterContent.State.Success: return "check"; + case FpsLimiterContent.State.Normal: + default: return "save"; + } + enabled: root.currentState === FpsLimiterContent.State.Normal && fpsField.text.length > 0 + onClicked: { + root.applyLimit(); + } + } + } +} diff --git a/dots/.config/quickshell/ii/modules/overlay/recorder/Recorder.qml b/dots/.config/quickshell/ii/modules/overlay/recorder/Recorder.qml index 78d747ed6..07e7c8d9f 100644 --- a/dots/.config/quickshell/ii/modules/overlay/recorder/Recorder.qml +++ b/dots/.config/quickshell/ii/modules/overlay/recorder/Recorder.qml @@ -13,6 +13,7 @@ StyledOverlayWidget { contentItem: Rectangle { id: contentItem anchors.centerIn: parent + radius: root.contentRadius color: Appearance.m3colors.m3surfaceContainer property real padding: 8 implicitHeight: contentColumn.implicitHeight + padding * 2 @@ -75,7 +76,7 @@ StyledOverlayWidget { colRipple: Appearance.colors.colLayer3Active onClicked: { GlobalStates.overlayOpen = false; - Qt.openUrlExternally(Directories.videos); + Qt.openUrlExternally(`file://${Config.options.screenRecord.savePath}`); } contentItem: Row { anchors.centerIn: parent diff --git a/dots/.config/quickshell/ii/modules/overlay/resources/Resources.qml b/dots/.config/quickshell/ii/modules/overlay/resources/Resources.qml index f5f5ff286..fe32d2a91 100644 --- a/dots/.config/quickshell/ii/modules/overlay/resources/Resources.qml +++ b/dots/.config/quickshell/ii/modules/overlay/resources/Resources.qml @@ -39,6 +39,7 @@ StyledOverlayWidget { id: contentItem anchors.centerIn: parent color: Appearance.m3colors.m3surfaceContainer + radius: root.contentRadius property real padding: 4 implicitWidth: 350 implicitHeight: 200 @@ -49,7 +50,7 @@ StyledOverlayWidget { fill: parent margins: parent.padding } - spacing: 10 + spacing: 8 SecondaryTabBar { id: tabBar diff --git a/dots/.config/quickshell/ii/modules/overlay/volumeMixer/VolumeMixer.qml b/dots/.config/quickshell/ii/modules/overlay/volumeMixer/VolumeMixer.qml index 14aa53006..d08b22a8d 100644 --- a/dots/.config/quickshell/ii/modules/overlay/volumeMixer/VolumeMixer.qml +++ b/dots/.config/quickshell/ii/modules/overlay/volumeMixer/VolumeMixer.qml @@ -1,7 +1,10 @@ import QtQuick +import QtQuick.Controls import QtQuick.Layouts import Quickshell +import qs.services import qs.modules.common +import qs.modules.common.widgets import qs.modules.overlay import qs.modules.sidebarRight.volumeMixer @@ -10,15 +13,69 @@ StyledOverlayWidget { contentItem: Rectangle { anchors.centerIn: parent color: Appearance.m3colors.m3surfaceContainer - property real padding: 16 + radius: root.contentRadius + property real padding: 6 implicitHeight: 600 implicitWidth: 350 - VolumeDialogContent { - anchors.fill: parent - anchors.margins: parent.padding - isSink: true - } + ColumnLayout { + id: contentColumn + anchors { + fill: parent + margins: parent.padding + } + spacing: 8 + SecondaryTabBar { + id: tabBar + + currentIndex: Persistent.states.overlay.volumeMixer.tabIndex + onCurrentIndexChanged: { + Persistent.states.overlay.volumeMixer.tabIndex = tabBar.currentIndex; + } + + SecondaryTabButton { + buttonIcon: "media_output" + buttonText: Translation.tr("Output") + } + SecondaryTabButton { + buttonIcon: "mic" + buttonText: Translation.tr("Input") + } + } + SwipeView { + id: swipeView + Layout.fillWidth: true + Layout.fillHeight: true + currentIndex: Persistent.states.overlay.volumeMixer.tabIndex + onCurrentIndexChanged: { + Persistent.states.overlay.volumeMixer.tabIndex = swipeView.currentIndex; + } + clip: true + + PaddedVolumeDialogContent { + isSink: true + } + PaddedVolumeDialogContent { + isSink: false + } + } + } + } + + component PaddedVolumeDialogContent: Item { + id: paddedVolumeDialogContent + property alias isSink: volDialogContent.isSink + property real padding: 12 + implicitWidth: volDialogContent.implicitWidth + padding * 2 + implicitHeight: volDialogContent.implicitHeight + padding * 2 + + VolumeDialogContent { + id: volDialogContent + anchors { + fill: parent + margins: paddedVolumeDialogContent.padding + } + } } } diff --git a/dots/.config/quickshell/ii/modules/sidebarRight/volumeMixer/VolumeDialogContent.qml b/dots/.config/quickshell/ii/modules/sidebarRight/volumeMixer/VolumeDialogContent.qml index 56f524968..a276128d9 100644 --- a/dots/.config/quickshell/ii/modules/sidebarRight/volumeMixer/VolumeDialogContent.qml +++ b/dots/.config/quickshell/ii/modules/sidebarRight/volumeMixer/VolumeDialogContent.qml @@ -29,6 +29,7 @@ ColumnLayout { Layout.topMargin: -22 Layout.leftMargin: 0 Layout.rightMargin: 0 + color: Appearance.colors.colOutlineVariant } DialogSectionListView { @@ -56,6 +57,7 @@ ColumnLayout { Layout.topMargin: -22 Layout.leftMargin: 0 Layout.rightMargin: 0 + color: Appearance.colors.colOutlineVariant } DialogSectionListView { diff --git a/dots/.config/quickshell/ii/modules/sidebarRight/volumeMixer/VolumeMixerEntry.qml b/dots/.config/quickshell/ii/modules/sidebarRight/volumeMixer/VolumeMixerEntry.qml index 503f83af0..a00335fb0 100644 --- a/dots/.config/quickshell/ii/modules/sidebarRight/volumeMixer/VolumeMixerEntry.qml +++ b/dots/.config/quickshell/ii/modules/sidebarRight/volumeMixer/VolumeMixerEntry.qml @@ -28,10 +28,10 @@ Item { sourceSize.height: size source: { let icon; - icon = AppSearch.guessIcon(root.node.properties["application.icon-name"]); + icon = AppSearch.guessIcon(root.node?.properties["application.icon-name"] ?? ""); if (AppSearch.iconExists(icon)) return Quickshell.iconPath(icon, "image-missing"); - icon = AppSearch.guessIcon(root.node.properties["node.name"]); + icon = AppSearch.guessIcon(root.node?.properties["node.name"] ?? ""); return Quickshell.iconPath(icon, "image-missing"); } } @@ -47,7 +47,7 @@ Item { elide: Text.ElideRight text: { // application.name -> description -> name - const app = root.node.properties["application.name"] ?? (root.node.description != "" ? root.node.description : root.node.name); + const app = root.node?.properties["application.name"] ?? (root.node.description != "" ? root.node.description : root.node.name); const media = root.node.properties["media.name"]; return media != undefined ? `${app} • ${media}` : app; } @@ -55,7 +55,7 @@ Item { StyledSlider { id: slider - value: root.node.audio.volume + value: root.node?.audio.volume ?? 0 onMoved: root.node.audio.volume = value configuration: StyledSlider.Configuration.S } diff --git a/dots/.config/quickshell/ii/services/ResourceUsage.qml b/dots/.config/quickshell/ii/services/ResourceUsage.qml index e42022cb5..df823adf7 100644 --- a/dots/.config/quickshell/ii/services/ResourceUsage.qml +++ b/dots/.config/quickshell/ii/services/ResourceUsage.qml @@ -67,7 +67,6 @@ Singleton { // Reload files fileMeminfo.reload() fileStat.reload() - fileCpuinfo.reload() // Parse memory and swap usage const textMeminfo = fileMeminfo.text() @@ -93,29 +92,6 @@ Singleton { previousCpuStats = { total, idle } } - // Parse max CPU frequency - const textCpuinfo = fileCpuinfo.text() - // Try to find 'cpu max MHz', fallback to highest 'cpu MHz' - let maxMHz = 0 - let match - // Try cpu max MHz (modern kernels) - match = textCpuinfo.match(/cpu max MHz\s*:\s*([\d.]+)/) - if (match) { - maxMHz = Number(match[1]) - } else { - // Fallback: find all cpu MHz lines and take the max - let mhzRegex = /cpu MHz\s*:\s*([\d.]+)/g - let mhzMatch - let mhzList = [] - while ((mhzMatch = mhzRegex.exec(textCpuinfo)) !== null) { - mhzList.push(Number(mhzMatch[1])) - } - if (mhzList.length > 0) { - maxMHz = Math.max.apply(null, mhzList) - } - } - root.maxAvailableCpuString = maxMHz > 0 ? (maxMHz / 1000).toFixed(1) + "GHz" : "--" - root.updateHistories() interval = Config.options?.resources?.updateInterval ?? 3000 } @@ -123,5 +99,16 @@ Singleton { FileView { id: fileMeminfo; path: "/proc/meminfo" } FileView { id: fileStat; path: "/proc/stat" } - FileView { id: fileCpuinfo; path: "/proc/cpuinfo" } + + Process { + id: findCpuMaxFreqProc + command: ["bash", "-c", "lscpu | grep 'CPU max MHz' | awk '{print $4}'"] + running: true + stdout: StdioCollector { + id: outputCollector + onStreamFinished: { + root.maxAvailableCpuString = (parseFloat(outputCollector.text) / 1000).toFixed(0) + " GHz" + } + } + } }