diff --git a/defaults/config.json b/defaults/config.json index 69721be91..5bff42cde 100644 --- a/defaults/config.json +++ b/defaults/config.json @@ -713,6 +713,7 @@ "invertPinnedItems": true, "monochromeIcons": true, "pinnedItems": [], + "ignoredItems": ["nm-applet", "network-manager-applet", "networkmanager-applet", "blueman-applet", "blueman"], "showItemId": false }, "utilButtons": { @@ -1099,14 +1100,6 @@ "android": { "columns": 4, "toggles": [ - { - "size": 1, - "type": "nightLight" - }, - { - "size": 1, - "type": "gameMode" - }, { "size": 1, "type": "screenSnip" @@ -1120,6 +1113,12 @@ "style": "android" }, "right": { + "controlsSectionOrder": [ + "toggles", + "sliders", + "media", + "quickActions" + ], "enabledWidgets": [ "dashboard", "calendar", @@ -1442,6 +1441,7 @@ "showItemId": false, "invertPinnedItems": true, "pinnedItems": [], + "ignoredItems": ["nm-applet", "network-manager-applet", "networkmanager-applet", "blueman-applet", "blueman"], "filterPassive": true }, "shellUpdates": { diff --git a/defaults/niri/config.d/30-window-rules.kdl b/defaults/niri/config.d/30-window-rules.kdl index 73a024873..60ef318af 100644 --- a/defaults/niri/config.d/30-window-rules.kdl +++ b/defaults/niri/config.d/30-window-rules.kdl @@ -33,11 +33,11 @@ window-rule { // Firefox / Zen: picture-in-picture window opens floating. window-rule { match app-id=r#"firefox$"# title="^Picture-in-Picture$" - open-floating true -} -window-rule { match app-id=r#"zen$"# title="^Picture-in-Picture$" open-floating true + default-floating-position x=24 y=24 relative-to="bottom-right" + default-column-width { fixed 640; } + default-window-height { fixed 360; } } window-rule { diff --git a/defaults/niri/config.d/40-environment.kdl b/defaults/niri/config.d/40-environment.kdl index 2b502517f..46bd3ca36 100644 --- a/defaults/niri/config.d/40-environment.kdl +++ b/defaults/niri/config.d/40-environment.kdl @@ -27,8 +27,8 @@ environment { // If plasma-integration is not installed, iNiR's setup patches this to "qt6ct". QT_QPA_PLATFORMTHEME "kde" - // Darkly is a modern Qt style that follows kdeglobals color schemes. - QT_STYLE_OVERRIDE "Darkly" + // Use the native Darkly style selected in qt6ct.conf + // QT_STYLE_OVERRIDE "kvantum" // Python venv used by iNiR scripts (color generation, AI features, translations). INIR_VENV "$HOME/.local/state/quickshell/.venv" diff --git a/defaults/niri/config.d/50-startup.kdl b/defaults/niri/config.d/50-startup.kdl index 6eaa141da..ba665046d 100644 --- a/defaults/niri/config.d/50-startup.kdl +++ b/defaults/niri/config.d/50-startup.kdl @@ -21,7 +21,8 @@ spawn-at-startup "bash" "-c" "wl-paste --type image --watch cliphist store &" // Polkit authentication agent — needed for GUI sudo prompts (software install, // disk mount, etc.). iNiR's setup auto-detects which agent is installed and // patches this line accordingly (KDE, GNOME, MATE, LXQt, or lxpolkit). -spawn-at-startup "/usr/lib/mate-polkit/polkit-mate-authentication-agent-1" +// spawn-at-startup "/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1" +spawn-at-startup "nm-applet" // ── iNiR shell ────────────────────────────────────────────────────────────── // iNiR is managed by the user systemd service (inir.service). diff --git a/defaults/niri/config.d/70-binds.kdl b/defaults/niri/config.d/70-binds.kdl index 03e634e8e..388bd05a7 100644 --- a/defaults/niri/config.d/70-binds.kdl +++ b/defaults/niri/config.d/70-binds.kdl @@ -73,7 +73,7 @@ binds { Mod+T { spawn "inir" "terminal"; } Mod+Return { spawn "inir" "terminal"; } - Super+E { spawn "nautilus"; } + Super+E { spawn "dolphin"; } Super+W { spawn "inir" "browser"; } // ═══════════════════════════════════════════════════════════════════════ diff --git a/dots/.config/niri/config.kdl b/dots/.config/niri/config.kdl index b090312c4..82c9f3090 100644 --- a/dots/.config/niri/config.kdl +++ b/dots/.config/niri/config.kdl @@ -198,7 +198,9 @@ environment { // Qt theming: kde platform reads colors from kdeglobals (requires plasma-integration pkg) // The installer falls back to qt6ct only if plasma-integration is not installed QT_QPA_PLATFORMTHEME "kde" - QT_STYLE_OVERRIDE "Darkly" + + // Use the native Darkly style selected in qt6ct.conf + // QT_STYLE_OVERRIDE "kvantum" // Quickshell Python virtual environment INIR_VENV "$HOME/.local/state/quickshell/.venv" @@ -213,7 +215,8 @@ environment { spawn-at-startup "bash" "-c" "systemctl --user import-environment XDG_MENU_PREFIX && kbuildsycoca6" spawn-at-startup "bash" "-c" "wl-paste --watch cliphist store &" -spawn-at-startup "/usr/lib/mate-polkit/polkit-mate-authentication-agent-1" +// spawn-at-startup "/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1" +spawn-at-startup "nm-applet" // iNiR is started by the user systemd service (inir.service), not spawn-at-startup. // ============================================================================ @@ -262,7 +265,7 @@ binds { // Applications (uses configured terminal from Settings) Mod+T { spawn "bash" "-c" "$HOME/.config/quickshell/inir/scripts/launch-terminal.sh"; } Mod+Return { spawn "bash" "-c" "$HOME/.config/quickshell/inir/scripts/launch-terminal.sh"; } - Super+E { spawn "nautilus"; } + Super+E { spawn "dolphin"; } Super+W { spawn "bash" "-c" "xdg-open https://"; } // Window management diff --git a/modules/background/Background.qml b/modules/background/Background.qml index d6a90c8cd..0ed65bfc8 100644 --- a/modules/background/Background.qml +++ b/modules/background/Background.qml @@ -1129,7 +1129,7 @@ Scope { anchorItem: desktopMenuAnchor popupAbove: false closeOnFocusLost: false - closeOnHoverLost: true + closeOnHoverLost: false model: [ { text: Translation.tr("Settings"), iconName: "settings", monochromeIcon: true, action: () => { Quickshell.execDetached([Quickshell.shellPath("scripts/inir"), "settings"]) } }, diff --git a/modules/bar/BarContent.qml b/modules/bar/BarContent.qml index 1cad8dab4..2a45b42d8 100644 --- a/modules/bar/BarContent.qml +++ b/modules/bar/BarContent.qml @@ -45,7 +45,7 @@ Item { // Bar content region anchorItem: barContextMenuAnchor popupAbove: Config.options?.bar?.bottom ?? false closeOnFocusLost: true - closeOnHoverLost: true + closeOnHoverLost: false model: [ { @@ -269,8 +269,8 @@ Item { // Bar content region MouseArea { anchors.fill: parent acceptedButtons: Qt.RightButton - onPressed: event => { - if (event.button === Qt.RightButton) + onReleased: mouse => { + if (mouse.button === Qt.RightButton) GlobalStates.overviewOpen = !GlobalStates.overviewOpen; } } @@ -538,11 +538,11 @@ Item { // Bar content region onScrollDown: root.performScrollAction(root.leftAction, false) onScrollUp: root.performScrollAction(root.leftAction, true) onMovedAway: root.closeOSD(root.leftAction) - onPressed: event => { - if (event.button === Qt.LeftButton) + onReleased: mouse => { + if (mouse.button === Qt.LeftButton) GlobalStates.sidebarLeftOpen = !GlobalStates.sidebarLeftOpen; - else if (event.button === Qt.RightButton) - root.openBarContextMenu(event.x, event.y, barLeftSideMouseArea) + else if (mouse.button === Qt.RightButton) + root.openBarContextMenu(mouse.x, mouse.y, barLeftSideMouseArea) } // ScrollHint as overlay - at the inner edge of the margin space @@ -704,8 +704,8 @@ Item { // Bar content region anchors.fill: rightCenterGroupPill acceptedButtons: Qt.LeftButton | Qt.RightButton z: -1 - onPressed: event => { - if (event.button === Qt.RightButton) { + onReleased: mouse => { + if (mouse.button === Qt.RightButton) { GlobalStates.controlPanelOpen = !GlobalStates.controlPanelOpen; } else { GlobalStates.sidebarRightOpen = !GlobalStates.sidebarRightOpen; @@ -764,11 +764,11 @@ Item { // Bar content region onScrollDown: root.performScrollAction(root.rightAction, false) onScrollUp: root.performScrollAction(root.rightAction, true) onMovedAway: root.closeOSD(root.rightAction) - onPressed: event => { - if (event.button === Qt.LeftButton) { + onReleased: mouse => { + if (mouse.button === Qt.LeftButton) { GlobalStates.sidebarRightOpen = !GlobalStates.sidebarRightOpen; - } else if (event.button === Qt.RightButton) { - root.openBarContextMenu(event.x, event.y, barRightSideMouseArea) + } else if (mouse.button === Qt.RightButton) { + root.openBarContextMenu(mouse.x, mouse.y, barRightSideMouseArea) } } @@ -939,6 +939,49 @@ Item { // Bar content region iconSize: Appearance.font.pixelSize.larger color: rightSidebarButton.colText Layout.rightMargin: BluetoothStatus.available ? indicatorsRowLayout.realSpacing : 0 + + HoverHandler { + id: wifiHover + onHoveredChanged: { + if (hovered) { + Network.refreshActiveNetworkDetails(); + } + } + } + + StyledToolTip { + extraVisibleCondition: wifiHover.hovered + text: { + if (!Network.wifiEnabled) return Translation.tr("Wi-Fi is disabled"); + if (Network.ethernet) return Translation.tr("Ethernet connected"); + // Show disconnected/connecting/limited states clearly + if (Network.wifiStatus === "disconnected" || (!Network.wifi && !Network.ethernet)) + return Translation.tr("Not connected"); + if (Network.wifiStatus === "connecting") + return Translation.tr("Connecting…"); + if (Network.wifiStatus === "disabled") + return Translation.tr("Wi-Fi is disabled"); + if (!Network.networkName) return Translation.tr("Not connected"); + let lines = [Translation.tr("Connected to %1").arg(Network.networkName)]; + // Show limited connectivity warning inline + if (Network.wifiStatus === "limited") + lines.push(Translation.tr("⚠ Limited connectivity (no internet)")); + if (Network.active) { + lines.push(Network.networkStrength + "%"); + if (Network.active.frequency) { + let ghz = Network.active.frequency > 5900 ? "6 GHz" : (Network.active.frequency > 4000 ? "5 GHz" : "2.4 GHz"); + lines.push(ghz + " (" + Network.active.frequency + " MHz)"); + } + if (Network.active.rate) { + lines.push(Network.active.rate); + } + if (Network.active.bssid) { + lines.push(Network.active.bssid); + } + } + return lines.join(" | "); + } + } } Revealer { reveal: BluetoothStatus.available @@ -947,6 +990,25 @@ Item { // Bar content region text: BluetoothStatus.activeIcon iconSize: Appearance.font.pixelSize.larger color: rightSidebarButton.colText + + HoverHandler { + id: btHover + } + + StyledToolTip { + extraVisibleCondition: btHover.hovered + text: { + if (!BluetoothStatus.enabled) return Translation.tr("Bluetooth is disabled"); + if (!BluetoothStatus.connected) return Translation.tr("Bluetooth disconnected"); + let device = BluetoothStatus.firstActiveDevice; + if (!device) return Translation.tr("Bluetooth connected"); + let devInfo = Translation.tr("Bluetooth Active") + " • " + (device.name || Translation.tr("Unknown device")); + if (device.batteryAvailable) { + devInfo += " 🔋 " + Math.round(device.battery * 100) + "%"; + } + return devInfo; + } + } } } } diff --git a/modules/bar/BarTaskbarButton.qml b/modules/bar/BarTaskbarButton.qml index 6c27edf0d..d5af66ee7 100644 --- a/modules/bar/BarTaskbarButton.qml +++ b/modules/bar/BarTaskbarButton.qml @@ -348,7 +348,7 @@ RippleButton { // Monochrome overlay Loader { - active: Config.options?.dock?.monochromeIcons ?? false + active: (Config.options?.dock?.monochromeIcons ?? false) && taskbarIcon.source.toString().includes(Config.options?.iconTheme ?? "yet-another-monochrome-icon-set") anchors.fill: iconLoader sourceComponent: Item { Desaturate { diff --git a/modules/bar/BatteryIndicator.qml b/modules/bar/BatteryIndicator.qml index a67b3f465..64210241d 100644 --- a/modules/bar/BatteryIndicator.qml +++ b/modules/bar/BatteryIndicator.qml @@ -6,6 +6,7 @@ import QtQuick.Layouts MouseArea { id: root + acceptedButtons: Qt.NoButton property bool borderless: Config.options.bar.borderless readonly property var chargeState: Battery.chargeState readonly property bool isCharging: Battery.isCharging diff --git a/modules/bar/Resources.qml b/modules/bar/Resources.qml index 53bf87db9..230037543 100644 --- a/modules/bar/Resources.qml +++ b/modules/bar/Resources.qml @@ -5,6 +5,7 @@ import QtQuick.Layouts MouseArea { id: root + acceptedButtons: Qt.NoButton property bool borderless: Config.options?.bar?.borderless ?? false property bool alwaysShowAllResources: false implicitWidth: rowLayout.implicitWidth + rowLayout.anchors.leftMargin + rowLayout.anchors.rightMargin diff --git a/modules/bar/SysTray.qml b/modules/bar/SysTray.qml index cd94cc9de..35fb1b716 100644 --- a/modules/bar/SysTray.qml +++ b/modules/bar/SysTray.qml @@ -50,8 +50,24 @@ Item { return item && item.id; } + function isIgnored(item) { + if (!isValidItem(item)) return true; + const id = (item.id || "").toLowerCase(); + const title = (item.title || "").toLowerCase(); + const tooltip = (item.tooltipTitle || "").toLowerCase(); + const ignored = Config.options?.bar?.tray?.ignoredItems + ?? ["nm-applet", "network-manager-applet", "networkmanager-applet"]; + for (let i = 0; i < ignored.length; i++) { + const pattern = ignored[i].toLowerCase(); + if (id.indexOf(pattern) !== -1 || title.indexOf(pattern) !== -1 || tooltip.indexOf(pattern) !== -1) { + return true; + } + } + return false; + } + property list itemsInUserList: SystemTray.items.values.filter(i => { - if (!isValidItem(i)) return false; + if (!isValidItem(i) || isIgnored(i)) return false; const id = (i.id || "").toLowerCase(); const title = (i.title || "").toLowerCase(); const isSpotify = id.indexOf("spotify") !== -1 || title.indexOf("spotify") !== -1; @@ -59,7 +75,7 @@ Item { && (!smartTray || i.status !== Status.Passive || isSpotify); }) property list itemsNotInUserList: SystemTray.items.values.filter(i => { - if (!isValidItem(i)) return false; + if (!isValidItem(i) || isIgnored(i)) return false; const id = (i.id || "").toLowerCase(); const title = (i.title || "").toLowerCase(); const isSpotify = id.indexOf("spotify") !== -1 || title.indexOf("spotify") !== -1; diff --git a/modules/bar/SysTrayItem.qml b/modules/bar/SysTrayItem.qml index 2de2c7882..458c05029 100644 --- a/modules/bar/SysTrayItem.qml +++ b/modules/bar/SysTrayItem.qml @@ -19,9 +19,9 @@ MouseArea { hoverEnabled: true acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton - implicitWidth: 18 - implicitHeight: 18 - onPressed: (event) => { + implicitWidth: 24 + implicitHeight: 24 + onReleased: (event) => { switch (event.button) { case Qt.LeftButton: { // Smart toggle: click to show, click again to minimize @@ -102,15 +102,15 @@ MouseArea { visible: !(Config.options?.bar?.tray?.monochromeIcons ?? false) source: root.item?.icon ?? "" anchors.centerIn: parent - width: parent.width - height: parent.height + width: 18 + height: 18 } Loader { active: Config.options?.bar?.tray?.monochromeIcons ?? false anchors.centerIn: parent - width: root.width - height: root.height + width: 18 + height: 18 sourceComponent: Item { IconImage { id: tintedIcon diff --git a/modules/bar/SysTrayMenu.qml b/modules/bar/SysTrayMenu.qml index d9547fe9f..dd7ee98d1 100644 --- a/modules/bar/SysTrayMenu.qml +++ b/modules/bar/SysTrayMenu.qml @@ -49,6 +49,7 @@ PopupWindow { // Fullscreen transparent backdrop for Niri to detect clicks outside PanelWindow { id: clickOutsideBackdrop + screen: root.screen visible: root.visible && CompositorService.isNiri color: "transparent" exclusiveZone: 0 diff --git a/modules/bar/Workspaces.qml b/modules/bar/Workspaces.qml index 820db2bb4..1595b9b00 100644 --- a/modules/bar/Workspaces.qml +++ b/modules/bar/Workspaces.qml @@ -489,7 +489,7 @@ Item { } Loader { - active: wsConfig.monochromeIcons + active: wsConfig.monochromeIcons && mainAppIcon.source.toString().includes(Config.options?.iconTheme ?? "yet-another-monochrome-icon-set") anchors.fill: mainAppIcon sourceComponent: Item { Desaturate { @@ -660,7 +660,7 @@ Item { } Loader { - active: wsConfig.monochromeIcons + active: wsConfig.monochromeIcons && columnAppIcon.source.toString().includes(Config.options?.iconTheme ?? "yet-another-monochrome-icon-set") anchors.fill: columnAppIcon sourceComponent: Item { Desaturate { diff --git a/modules/common/Config.qml b/modules/common/Config.qml index bd9beb97f..6d796d1c3 100644 --- a/modules/common/Config.qml +++ b/modules/common/Config.qml @@ -1259,6 +1259,7 @@ Singleton { property bool showItemId: false property bool invertPinnedItems: true // Makes the below a whitelist for the tray and blacklist for the pinned area property list pinnedItems: [] + property list ignoredItems: ["nm-applet", "network-manager-applet", "networkmanager-applet", "blueman-applet", "blueman"] property bool filterPassive: true } property JsonObject workspaces: JsonObject { @@ -1660,6 +1661,7 @@ Singleton { property string engineBaseUrl: "https://www.google.com/search?q=" property list excludedSites: ["quora.com", "facebook.com"] property bool sloppy: false // Uses levenshtein distance based scoring instead of fuzzy sort. Very weird. + property bool monochromeIcons: true property JsonObject prefix: JsonObject { property bool showDefaultActionsWithoutPrefix: true property string action: "/" @@ -2052,6 +2054,7 @@ Singleton { property bool showItemId: false property bool invertPinnedItems: true property list pinnedItems: [] + property list ignoredItems: ["nm-applet", "network-manager-applet", "networkmanager-applet", "blueman-applet", "blueman"] property bool filterPassive: true } property JsonObject updates: JsonObject { diff --git a/modules/common/models/quickToggles/NetworkToggle.qml b/modules/common/models/quickToggles/NetworkToggle.qml index d3aa29b58..03c99ecc2 100644 --- a/modules/common/models/quickToggles/NetworkToggle.qml +++ b/modules/common/models/quickToggles/NetworkToggle.qml @@ -8,7 +8,21 @@ import qs.modules.common.widgets QuickToggleModel { name: Translation.tr("Internet") statusText: Network.networkName - tooltipText: Translation.tr("%1 | Right-click to configure").arg(Network.networkName) + tooltipText: { + if (!Network.wifiEnabled) return Translation.tr("Wi-Fi is disabled"); + if (Network.ethernet) return Translation.tr("Ethernet connected"); + if (!Network.networkName) return Translation.tr("Not connected"); + let info = Network.networkName; + if (Network.active) { + info += " (" + Network.networkStrength + "%)"; + if (Network.active.rate) info += " | " + Network.active.rate; + if (Network.active.frequency) { + let ghz = Network.active.frequency > 5900 ? "6 GHz" : (Network.active.frequency > 4000 ? "5 GHz" : "2.4 GHz"); + info += " | " + ghz; + } + } + return Translation.tr("%1 | Right-click to configure").arg(info); + } icon: Network.materialSymbol toggled: Network.wifiStatus !== "disabled" diff --git a/modules/common/widgets/ContextMenu.qml b/modules/common/widgets/ContextMenu.qml index b8db02d24..dd96e39ef 100644 --- a/modules/common/widgets/ContextMenu.qml +++ b/modules/common/widgets/ContextMenu.qml @@ -286,6 +286,7 @@ Loader { PanelWindow { id: clickOutsideBackdrop + screen: popupWindow.screen visible: popupWindow.visible && CompositorService.isNiri && root.closeOnFocusLost color: "transparent" exclusiveZone: 0 diff --git a/modules/common/widgets/NotificationItem.qml b/modules/common/widgets/NotificationItem.qml index 59fc34f11..959003602 100644 --- a/modules/common/widgets/NotificationItem.qml +++ b/modules/common/widgets/NotificationItem.qml @@ -71,10 +71,10 @@ Item { // Notification item area anchors.leftMargin: root.expanded ? -root.dismissOvershoot : 0 interactive: expanded automaticallyReset: false - acceptedButtons: Qt.LeftButton | Qt.MiddleButton + acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton onClicked: (mouse) => { - if (mouse.button === Qt.MiddleButton) { + if (mouse.button === Qt.MiddleButton || mouse.button === Qt.RightButton) { root.destroyWithAnimation(); } } diff --git a/modules/dock/DockAppButton.qml b/modules/dock/DockAppButton.qml index 410aa8f43..1310eb4ac 100644 --- a/modules/dock/DockAppButton.qml +++ b/modules/dock/DockAppButton.qml @@ -445,7 +445,7 @@ DockButton { } Loader { - active: Config.options?.dock?.monochromeIcons ?? false + active: (Config.options?.dock?.monochromeIcons ?? false) && iconImageLoader.source.toString().includes(Config.options?.iconTheme ?? "yet-another-monochrome-icon-set") anchors.fill: iconImageLoader sourceComponent: Item { Desaturate { diff --git a/modules/lock/Lock.qml b/modules/lock/Lock.qml index ffd8518b4..912bbd18e 100644 --- a/modules/lock/Lock.qml +++ b/modules/lock/Lock.qml @@ -174,6 +174,13 @@ Scope { id: lock locked: GlobalStates.screenLocked + onSecureChanged: { + if (!secure && GlobalStates.screenLocked) { + console.warn("[Lock] Compositor cancelled Wayland session lock while screenLocked was true! Activating fallback locker (swaylock/hyprlock) to keep session secure.") + root.useFallbackLock() + } + } + WlSessionLockSurface { id: lockSurface // Use colLayer0 as transitional background - actual lock surface has its own bg @@ -257,10 +264,11 @@ Scope { delegate: Scope { required property ShellScreen modelData property bool shouldPush: GlobalStates.screenLocked && CompositorService.isHyprland - property string targetMonitorName: modelData.name - property int verticalMovementDistance: modelData.height - property int horizontalSqueeze: modelData.width * 0.2 + property string targetMonitorName: modelData ? modelData.name : "" + property int verticalMovementDistance: modelData ? modelData.height : 0 + property int horizontalSqueeze: modelData ? modelData.width * 0.2 : 0 onShouldPushChanged: { + if (!modelData) return; if (shouldPush) { root.saveWindowPositionAndTile(); Quickshell.execDetached(["hyprctl", "keyword", "monitor", `${targetMonitorName}, addreserved, ${verticalMovementDistance}, ${-verticalMovementDistance}, ${horizontalSqueeze}, ${horizontalSqueeze}`]) @@ -298,6 +306,10 @@ Scope { target: "lock" function activate(): void { + if (Config.options?.lock?.useHyprlock ?? false) { + Quickshell.execDetached(["/usr/bin/bash", "-lc", "/usr/bin/pidof hyprlock || /usr/bin/hyprlock"]); + return; + } if (GlobalStates.screenLocked || root._lockActivating) return; lockActivateDelay.restart(); diff --git a/modules/overview/SearchItem.qml b/modules/overview/SearchItem.qml index a0625ef6e..66ad607cb 100644 --- a/modules/overview/SearchItem.qml +++ b/modules/overview/SearchItem.qml @@ -7,6 +7,7 @@ import qs.modules.common.widgets import qs.modules.common.functions import QtQuick import QtQuick.Layouts +import Qt5Compat.GraphicalEffects import Quickshell import Quickshell.Widgets @@ -167,10 +168,33 @@ RippleButton { Component { id: iconImageComponent - IconImage { - source: Quickshell.iconPath(root.itemIcon, "image-missing") - width: 35 - height: 35 + Item { + implicitWidth: 35 + implicitHeight: 35 + IconImage { + id: baseIcon + source: Quickshell.iconPath(root.itemIcon, "image-missing") + anchors.fill: parent + } + Loader { + active: (Config.options?.search?.monochromeIcons ?? true) && baseIcon.source.toString().includes(Config.options?.iconTheme ?? "yet-another-monochrome-icon-set") + anchors.fill: parent + sourceComponent: Item { + anchors.fill: parent + Desaturate { + id: desaturatedIcon + visible: false + anchors.fill: parent + source: baseIcon + desaturation: 1.0 + } + ColorOverlay { + anchors.fill: desaturatedIcon + source: desaturatedIcon + color: root.isHighlighted ? root.selectedTextColor : root.normalTextColor + } + } + } } } diff --git a/modules/screenCorners/ScreenCorners.qml b/modules/screenCorners/ScreenCorners.qml index 48aa789ff..9d4d85108 100644 --- a/modules/screenCorners/ScreenCorners.qml +++ b/modules/screenCorners/ScreenCorners.qml @@ -32,7 +32,7 @@ Scope { readonly property bool cornerOpenEnabled: Config?.options?.sidebar?.cornerOpen?.enable ?? false readonly property bool cornerOpenAtBottom: Config?.options?.sidebar?.cornerOpen?.bottom ?? false readonly property bool cornerOpenMatchesPosition: cornerOpenAtBottom === cornerWidget.isBottom - readonly property bool shouldShowCornerOpen: cornerOpenEnabled && cornerOpenMatchesPosition && !fullscreen && !GameMode.shouldHidePanels + readonly property bool shouldShowCornerOpen: cornerOpenEnabled && cornerOpenMatchesPosition && !fullscreen && !GameMode.shouldHidePanels && corner !== RoundCorner.CornerEnum.TopLeft visible: !GameMode.shouldHidePanels && (showFakeRounding || shouldShowCornerOpen) diff --git a/modules/settings/GeneralConfig.qml b/modules/settings/GeneralConfig.qml index 6df481359..7e5266e79 100644 --- a/modules/settings/GeneralConfig.qml +++ b/modules/settings/GeneralConfig.qml @@ -57,7 +57,7 @@ ContentPage { text: Translation.tr("Volume limit") value: Config.options?.audio?.protection?.maxAllowed ?? 0 from: 0 - to: 154 // pavucontrol allows up to 153% + to: 200 // pavucontrol allows up to 153% stepSize: 2 onValueChanged: { Config.setNestedValue("audio.protection.maxAllowed", value); diff --git a/modules/sidebarLeft/SidebarLeftContent.qml b/modules/sidebarLeft/SidebarLeftContent.qml index ef7a2a8d6..0d150e185 100644 --- a/modules/sidebarLeft/SidebarLeftContent.qml +++ b/modules/sidebarLeft/SidebarLeftContent.qml @@ -77,7 +77,7 @@ Item { if (root.wallhavenEnabled) result.push({ icon: "collections", name: Translation.tr("Wallhaven") }) if (root.ytMusicEnabled) result.push({ icon: "library_music", name: Translation.tr("YT Music") }) if (root.toolsEnabled) result.push({ icon: "build", name: Translation.tr("Tools") }) - if (root.softwareEnabled) result.push({ icon: "store", name: Translation.tr("Software") }) + if (root.softwareEnabled) result.push({ icon: "apps", name: Translation.tr("Applications") }) // DISABLED: webapps — requires quickshell-webengine rebuild // if (root.pluginsEnabled) result.push({ icon: "extension", name: Translation.tr("Web Apps") }) return result @@ -293,7 +293,7 @@ Item { case "collections": return wallhavenComp case "library_music": return ytMusicComp case "build": return toolsComp - case "store": return softwareComp + case "apps": return softwareComp // DISABLED: webapps // case "extension": return pluginsComp default: return null diff --git a/modules/sidebarLeft/SoftwareView.qml b/modules/sidebarLeft/SoftwareView.qml index bf9487d07..a780a6480 100644 --- a/modules/sidebarLeft/SoftwareView.qml +++ b/modules/sidebarLeft/SoftwareView.qml @@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts import Quickshell +import Qt5Compat.GraphicalEffects import qs.modules.common import qs.modules.common.widgets import qs.modules.common.functions @@ -14,10 +15,6 @@ Item { readonly property color colText: Appearance.angelEverywhere ? Appearance.angel.colText : Appearance.inirEverywhere ? Appearance.inir.colText : Appearance.colors.colOnLayer1 readonly property color colTextSecondary: Appearance.inirEverywhere ? Appearance.inir.colTextSecondary : Appearance.colors.colSubtext - readonly property color colBg: Appearance.angelEverywhere ? Appearance.angel.colGlassCard - : Appearance.inirEverywhere ? Appearance.inir.colLayer1 - : Appearance.auroraEverywhere ? "transparent" - : Appearance.colors.colLayer1 readonly property color colBgHover: Appearance.angelEverywhere ? Appearance.angel.colGlassCardHover : Appearance.inirEverywhere ? Appearance.inir.colLayer1Hover : Appearance.auroraEverywhere ? Appearance.aurora.colSubSurface @@ -25,10 +22,50 @@ Item { readonly property real radius: Appearance.angelEverywhere ? Appearance.angel.roundingSmall : Appearance.inirEverywhere ? Appearance.inir.roundingSmall : Appearance.rounding.verysmall - Component.onCompleted: { - if (AppCatalog.packageManager === "unknown") { - AppCatalog.refresh() + property string searchQuery: "" + property string selectedCategory: "all" + + readonly property var categories: ["internet", "development", "gaming", "graphics", "office", "media", "system"] + + readonly property var filteredApps: { + const q = root.searchQuery.toLowerCase().trim() + const cat = root.selectedCategory + const all = DesktopEntries.applications.values + .filter(e => !e.noDisplay) + + let filtered = all + if (cat !== "all") { + filtered = filtered.filter(app => { + const appCats = Array.from(app.categories ?? []).map(c => c.toLowerCase()) + switch (cat) { + case "internet": + return appCats.some(c => c === "network" || c === "webbrowser" || c === "email" || c === "chat") + case "development": + return appCats.some(c => c === "development" || c === "ide" || c === "debugger" || c === "building") + case "gaming": + return appCats.some(c => c === "game" || c === "gaming" || c === "emulator") + case "graphics": + return appCats.some(c => c === "graphics" || c === "viewer" || c === "photography" || c === "design") + case "office": + return appCats.some(c => c === "office" || c === "dictionary" || c === "wordprocessor" || c === "spreadsheet") + case "media": + return appCats.some(c => c === "audiovideo" || c === "audio" || c === "video" || c === "player") + case "system": + return appCats.some(c => c === "system" || c === "settings" || c === "utility" || c === "terminal" || c === "filemanager") + default: + return false + } + }) + } + if (q.length > 0) { + filtered = filtered.filter(app => { + const name = (app.name ?? "").toLowerCase() + const desc = (app.comment ?? "").toLowerCase() + const gName = (app.genericName ?? "").toLowerCase() + return name.includes(q) || desc.includes(q) || gName.includes(q) + }) } + return filtered.sort((a, b) => (a.name || "").localeCompare(b.name || "")) } Flickable { @@ -48,8 +85,8 @@ Item { ToolbarTextField { Layout.fillWidth: true placeholderText: Translation.tr("Search apps...") - text: AppCatalog.searchQuery - onTextChanged: AppCatalog.searchQuery = text + text: root.searchQuery + onTextChanged: root.searchQuery = text } // ─── Category selector (ButtonGroup + GroupButton, scrollable with fade) ─ @@ -73,7 +110,7 @@ Item { property int clickIndex: -1 GroupButton { - toggled: AppCatalog.selectedCategory === "all" + toggled: root.selectedCategory === "all" bounce: true colBackground: Appearance.angelEverywhere ? Appearance.angel.colGlassCard : Appearance.inirEverywhere ? Appearance.inir.colLayer2 @@ -104,16 +141,16 @@ Item { } onClicked: { catGroup.clickIndex = 0 - AppCatalog.selectedCategory = "all" + root.selectedCategory = "all" } } Repeater { - model: AppCatalog.categories + model: root.categories delegate: GroupButton { required property string modelData required property int index - toggled: AppCatalog.selectedCategory === modelData + toggled: root.selectedCategory === modelData bounce: true colBackground: Appearance.angelEverywhere ? Appearance.angel.colGlassCard : Appearance.inirEverywhere ? Appearance.inir.colLayer2 @@ -144,7 +181,7 @@ Item { } onClicked: { catGroup.clickIndex = index + 1 - AppCatalog.selectedCategory = modelData + root.selectedCategory = modelData } } } @@ -181,118 +218,23 @@ Item { } } - // ─── PM info + refresh ─────────────────────────────── - RowLayout { - Layout.fillWidth: true - spacing: 4 - - MaterialSymbol { - text: "terminal" - iconSize: 14 - color: root.colTextSecondary - } - StyledText { - Layout.fillWidth: true - text: root._pmLabel() - font.pixelSize: Appearance.font.pixelSize.smaller - color: root.colTextSecondary - } - RippleButton { - implicitWidth: 28 - implicitHeight: 28 - buttonRadius: Appearance.rounding.full - enabled: !AppCatalog.checkingInstalled - - colBackgroundHover: root.colBgHover - - onClicked: AppCatalog.refresh() - - contentItem: MaterialSymbol { - anchors.centerIn: parent - text: "refresh" - iconSize: 16 - color: root.colText - - RotationAnimation on rotation { - running: AppCatalog.checkingInstalled - from: 0 - to: 360 - duration: 1000 - loops: Animation.Infinite - } - } - - StyledToolTip { text: Translation.tr("Refresh") } - } - } - - // ─── Loading state ─────────────────────────────────── - ColumnLayout { - Layout.fillWidth: true - Layout.topMargin: 40 - visible: AppCatalog.loading - spacing: 10 - - MaterialLoadingIndicator { - Layout.alignment: Qt.AlignHCenter - loading: true - implicitSize: 32 - } - StyledText { - Layout.alignment: Qt.AlignHCenter - text: Translation.tr("Loading catalog...") - font.pixelSize: Appearance.font.pixelSize.small - color: root.colTextSecondary - } - } - - // ─── PM not detected ───────────────────────────────── - ColumnLayout { - Layout.fillWidth: true - Layout.topMargin: 40 - visible: !AppCatalog.loading && AppCatalog.packageManager === "unknown" - spacing: 8 - - MaterialSymbol { - Layout.alignment: Qt.AlignHCenter - text: "error_outline" - iconSize: 48 - color: root.colTextSecondary - } - StyledText { - Layout.fillWidth: true - horizontalAlignment: Text.AlignHCenter - text: Translation.tr("No package manager detected") - font.pixelSize: Appearance.font.pixelSize.small - color: root.colText - } - StyledText { - Layout.fillWidth: true - horizontalAlignment: Text.AlignHCenter - text: Translation.tr("Install pacman, apt, or dnf") - font.pixelSize: Appearance.font.pixelSize.smaller - color: root.colTextSecondary - } - } - // ─── Empty search state ────────────────────────────── ColumnLayout { Layout.fillWidth: true Layout.topMargin: 40 - visible: !AppCatalog.loading && AppCatalog.packageManager !== "unknown" - && AppCatalog.filteredCatalog.length === 0 + visible: root.filteredApps.length === 0 spacing: 8 MaterialSymbol { Layout.alignment: Qt.AlignHCenter - text: AppCatalog.searchQuery.length > 0 ? "search_off" : "inventory_2" + text: root.searchQuery.length > 0 ? "search_off" : "inventory_2" iconSize: 48 color: root.colTextSecondary } StyledText { Layout.fillWidth: true horizontalAlignment: Text.AlignHCenter - text: AppCatalog.searchQuery.length > 0 + text: root.searchQuery.length > 0 ? Translation.tr("No apps match your search") : Translation.tr("No apps in this category") font.pixelSize: Appearance.font.pixelSize.small @@ -302,8 +244,7 @@ Item { // ─── App list ──────────────────────────────────────── Repeater { - model: (!AppCatalog.loading && AppCatalog.packageManager !== "unknown") - ? AppCatalog.filteredCatalog : [] + model: root.filteredApps delegate: AppCard { required property var modelData @@ -319,52 +260,30 @@ Item { function _categoryIcon(id: string): string { switch (id) { - case "browsers": return "language" - case "terminals": return "terminal" - case "editors": return "edit" - case "media": return "movie" - case "communication": return "chat" - case "system": return "settings" + case "internet": return "language" case "development": return "code" - case "theming": return "palette" - case "fonts": return "font_download" case "gaming": return "sports_esports" + case "graphics": return "palette" + case "office": return "edit_note" + case "media": return "movie" + case "system": return "settings" default: return "category" } } function _categoryLabel(id: string): string { switch (id) { - case "browsers": return Translation.tr("Browsers") - case "terminals": return Translation.tr("Terminals") - case "editors": return Translation.tr("Editors") - case "media": return Translation.tr("Media") - case "communication": return Translation.tr("Communication") - case "system": return Translation.tr("System") + case "internet": return Translation.tr("Internet") case "development": return Translation.tr("Development") - case "theming": return Translation.tr("Theming") - case "fonts": return Translation.tr("Fonts") case "gaming": return Translation.tr("Gaming") + case "graphics": return Translation.tr("Graphics") + case "office": return Translation.tr("Office") + case "media": return Translation.tr("Media") + case "system": return Translation.tr("System") default: return id } } - function _pmLabel(): string { - const pm = AppCatalog.packageManager - switch (pm) { - case "pacman": { - let label = "pacman" - if (AppCatalog.hasAurHelper) label += " + " + AppCatalog.aurHelper - if (AppCatalog.hasFlatpak) label += " + flatpak" - return label - } - case "apt": return AppCatalog.hasFlatpak ? "apt + flatpak" : "apt" - case "dnf": return AppCatalog.hasFlatpak ? "dnf + flatpak" : "dnf" - case "unknown": return Translation.tr("Detecting...") - default: return pm - } - } - // ═════════════════════════════════════════════════════════════════ // INLINE COMPONENT: AppCard // ═════════════════════════════════════════════════════════════════ @@ -374,10 +293,6 @@ Item { required property var app - readonly property bool isInstalled: AppCatalog.isInstalled(card.app?.id ?? "") - readonly property string installMethod: AppCatalog.getInstallMethod(card.app ?? {}) - readonly property bool isAvailable: AppCatalog.isAvailable(card.app ?? {}) - implicitHeight: 56 buttonRadius: root.radius @@ -389,49 +304,45 @@ Item { : Appearance.colors.colLayer1Active onClicked: { - if (card.isInstalled) { - AppCatalog.removeApp(card.app?.id ?? "") - } else if (card.isAvailable) { - AppCatalog.installApp(card.app?.id ?? "") - } + GlobalStates.sidebarLeftOpen = false + card.app.execute() } contentItem: RowLayout { spacing: 10 - // App icon — Quickshell.iconPath for theme lookup, MaterialSymbol fallback + // App icon — resolve using AppSearch helper Item { + id: iconContainer Layout.preferredWidth: 32 Layout.preferredHeight: 32 - property string _resolvedIcon: { - const di = card.app?.desktopIcon ?? "" - if (di.length === 0) return "" - const resolved = Quickshell.iconPath(di, "") - // Only use if it resolved to a real file path - if (resolved.length === 0) return "" - const str = resolved.toString() - if (str.startsWith("file://") || str.startsWith("/")) return resolved - return "" - } - - Image { + SmartAppIcon { id: appIcon anchors.fill: parent - source: parent._resolvedIcon - sourceSize: Qt.size(64, 64) - visible: parent._resolvedIcon.length > 0 && appIcon.status === Image.Ready - smooth: true - mipmap: true + icon: card.app?.icon || card.app?.name || "" + fallback: "application-x-executable" + iconSize: 32 + monochrome: false } - // Fallback: Material Symbol from catalog - MaterialSymbol { - anchors.centerIn: parent - visible: !appIcon.visible - text: card.app?.icon ?? "apps" - iconSize: 24 - color: root.colText + Loader { + active: (Config.options?.sidebar?.software?.monochromeIcons ?? true) && appIcon.resolvedSource.includes(Config.options?.iconTheme ?? "yet-another-monochrome-icon-set") + anchors.fill: parent + sourceComponent: Item { + Desaturate { + id: desaturatedIcon + visible: false + anchors.fill: parent + source: appIcon + desaturation: 1.0 + } + ColorOverlay { + anchors.fill: desaturatedIcon + source: desaturatedIcon + color: root.colText + } + } } } @@ -440,51 +351,18 @@ Item { Layout.fillWidth: true spacing: 1 - RowLayout { + StyledText { + text: card.app?.name ?? "" + font.pixelSize: Appearance.font.pixelSize.small + font.weight: Font.Medium + color: root.colText + elide: Text.ElideRight Layout.fillWidth: true - spacing: 6 - - StyledText { - text: card.app?.name ?? "" - font.pixelSize: Appearance.font.pixelSize.small - font.weight: Font.Medium - color: root.colText - elide: Text.ElideRight - Layout.fillWidth: true - } - - // Method / installed badge - Rectangle { - visible: card.installMethod.length > 0 || card.isInstalled - implicitWidth: badgeText.implicitWidth + 10 - implicitHeight: badgeText.implicitHeight + 4 - radius: height / 2 - color: card.isInstalled - ? (Appearance.inirEverywhere ? Appearance.inir.colPrimaryContainer - : Appearance.colors.colPrimaryContainer) - : (Appearance.inirEverywhere ? Appearance.inir.colLayer2 - : Appearance.auroraEverywhere ? Appearance.aurora.colSubSurface - : Appearance.colors.colSecondaryContainer) - - StyledText { - id: badgeText - anchors.centerIn: parent - text: card.isInstalled - ? Translation.tr("Installed") - : card.installMethod - font.pixelSize: Appearance.font.pixelSize.smallest - color: card.isInstalled - ? (Appearance.inirEverywhere ? Appearance.inir.colOnPrimaryContainer - : Appearance.colors.colOnPrimaryContainer) - : (Appearance.inirEverywhere ? Appearance.inir.colTextSecondary - : Appearance.colors.colOnSecondaryContainer) - } - } } StyledText { Layout.fillWidth: true - text: card.app?.description ?? "" + text: card.app?.comment ?? card.app?.genericName ?? "" font.pixelSize: Appearance.font.pixelSize.smaller color: root.colTextSecondary elide: Text.ElideRight @@ -494,23 +372,14 @@ Item { // Action icon MaterialSymbol { - text: card.isInstalled ? "check_circle" - : card.isAvailable ? "download" - : "block" + text: "arrow_forward" iconSize: 20 - fill: card.isInstalled ? 1 : 0 - animateFill: true - color: card.isInstalled - ? (Appearance.inirEverywhere ? Appearance.inir.colPrimary : Appearance.colors.colPrimary) - : card.isAvailable ? root.colText - : root.colTextSecondary + color: root.colTextSecondary } } StyledToolTip { - text: card.isInstalled ? Translation.tr("Click to remove") - : card.isAvailable ? Translation.tr("Click to install via %1").arg(card.installMethod) - : Translation.tr("Not available for your system") + text: Translation.tr("Launch %1").arg(card.app?.name ?? "") } } } diff --git a/modules/sidebarRight/bluetoothDevices/BluetoothDeviceItem.qml b/modules/sidebarRight/bluetoothDevices/BluetoothDeviceItem.qml index a41dd1198..a5f72151b 100644 --- a/modules/sidebarRight/bluetoothDevices/BluetoothDeviceItem.qml +++ b/modules/sidebarRight/bluetoothDevices/BluetoothDeviceItem.qml @@ -101,9 +101,18 @@ DialogListItem { Item { Layout.fillWidth: true } ActionButton { - buttonText: root.device?.connected ? Translation.tr("Disconnect") : Translation.tr("Connect") + id: connectBtn + property bool operationPending: false + buttonText: { + if (operationPending) { + return root.device?.connected ? Translation.tr("Disconnecting…") : Translation.tr("Connecting…"); + } + return root.device?.connected ? Translation.tr("Disconnect") : Translation.tr("Connect") + } onClicked: { + operationPending = true; + pendingTimeout.start(); if (root.device?.connected) { root.device.disconnect(); } else { @@ -111,6 +120,20 @@ DialogListItem { root.device.connect(); } } + + Connections { + target: root.device + function onConnectedChanged() { + connectBtn.operationPending = false; + pendingTimeout.stop(); + } + } + + Timer { + id: pendingTimeout + interval: 15000 + onTriggered: connectBtn.operationPending = false + } } Revealer { reveal: root.device?.paired ?? false diff --git a/modules/sidebarRight/quickToggles/androidStyle/AndroidNetworkToggle.qml b/modules/sidebarRight/quickToggles/androidStyle/AndroidNetworkToggle.qml index 0b17d4426..f70063221 100644 --- a/modules/sidebarRight/quickToggles/androidStyle/AndroidNetworkToggle.qml +++ b/modules/sidebarRight/quickToggles/androidStyle/AndroidNetworkToggle.qml @@ -8,14 +8,41 @@ AndroidQuickToggleButton { id: root name: Translation.tr("Internet") - statusText: Network.networkName + statusText: { + if (Network.wifiStatus === "limited") return Translation.tr("No internet"); + if (Network.wifiStatus === "connecting") return Translation.tr("Connecting…"); + if (Network.wifiStatus === "disconnected") return Translation.tr("Disconnected"); + if (Network.wifiStatus === "disabled") return Translation.tr("Disabled"); + return Network.networkName || Translation.tr("Not connected"); + } toggled: Network.wifiStatus !== "disabled" buttonIcon: Network.materialSymbol mainAction: () => Network.toggleWifi() altAction: () => root.openMenu() StyledToolTip { - text: Translation.tr("%1 | Right-click to configure").arg(Network.networkName) + text: { + if (!Network.wifiEnabled) return Translation.tr("Wi-Fi is disabled"); + if (Network.ethernet) return Translation.tr("Ethernet connected"); + if (Network.wifiStatus === "disconnected" || (!Network.wifi && !Network.ethernet)) + return Translation.tr("Not connected | Right-click to configure"); + if (Network.wifiStatus === "connecting") + return Translation.tr("Connecting… | Right-click to configure"); + if (Network.wifiStatus === "disabled") + return Translation.tr("Wi-Fi is disabled"); + if (!Network.networkName) return Translation.tr("Not connected | Right-click to configure"); + let info = Network.networkName; + if (Network.wifiStatus === "limited") + info += " (⚠ no internet)"; + if (Network.active) { + info += " (" + Network.networkStrength + "%)" + if (Network.active.rate) info += " | " + Network.active.rate; + if (Network.active.frequency) { + let ghz = Network.active.frequency > 5900 ? "6 GHz" : (Network.active.frequency > 4000 ? "5 GHz" : "2.4 GHz"); + info += " | " + ghz; + } + } + return Translation.tr("%1 | Right-click to configure").arg(info); + } } } - diff --git a/modules/sidebarRight/quickToggles/classicStyle/NetworkToggle.qml b/modules/sidebarRight/quickToggles/classicStyle/NetworkToggle.qml index ac5b28a81..276f340dd 100644 --- a/modules/sidebarRight/quickToggles/classicStyle/NetworkToggle.qml +++ b/modules/sidebarRight/quickToggles/classicStyle/NetworkToggle.qml @@ -15,6 +15,29 @@ QuickToggleButton { onClicked: Network.toggleWifi() // altAction is set by parent (ClassicQuickPanel opens dialog, others may open external app) StyledToolTip { - text: Translation.tr("%1 | Right-click to configure").arg(Network.networkName) + text: { + if (!Network.wifiEnabled) return Translation.tr("Wi-Fi is disabled"); + if (Network.ethernet) return Translation.tr("Ethernet connected"); + // Show special states clearly + if (Network.wifiStatus === "disconnected" || (!Network.wifi && !Network.ethernet)) + return Translation.tr("Not connected | Right-click to configure"); + if (Network.wifiStatus === "connecting") + return Translation.tr("Connecting… | Right-click to configure"); + if (Network.wifiStatus === "disabled") + return Translation.tr("Wi-Fi is disabled"); + if (!Network.networkName) return Translation.tr("Not connected | Right-click to configure"); + let info = Network.networkName; + if (Network.wifiStatus === "limited") + info += " (⚠ no internet)"; + if (Network.active) { + info += " (" + Network.networkStrength + "%)"; + if (Network.active.rate) info += " | " + Network.active.rate; + if (Network.active.frequency) { + let ghz = Network.active.frequency > 5900 ? "6 GHz" : (Network.active.frequency > 4000 ? "5 GHz" : "2.4 GHz"); + info += " | " + ghz; + } + } + return Translation.tr("%1 | Right-click to configure").arg(info); + } } } diff --git a/modules/sidebarRight/wifiNetworks/WifiNetworkItem.qml b/modules/sidebarRight/wifiNetworks/WifiNetworkItem.qml index f93e4a906..a604c76cc 100644 --- a/modules/sidebarRight/wifiNetworks/WifiNetworkItem.qml +++ b/modules/sidebarRight/wifiNetworks/WifiNetworkItem.qml @@ -9,10 +9,12 @@ import QtQuick.Layouts DialogListItem { id: root required property WifiAccessPoint wifiNetwork - enabled: !(Network.wifiConnectTarget === root.wifiNetwork && !wifiNetwork?.active) + enabled: true active: (wifiNetwork?.askingPassword || wifiNetwork?.active) ?? false onClicked: { + if (wifiNetwork?.active) return; // already connected + if (Network.wifiConnectTarget === root.wifiNetwork) return; // already connecting Network.connectToWifiNetwork(wifiNetwork); } @@ -35,36 +37,108 @@ DialogListItem { text: strength > 80 ? "signal_wifi_4_bar" : strength > 60 ? "network_wifi_3_bar" : strength > 40 ? "network_wifi_2_bar" : strength > 20 ? "network_wifi_1_bar" : "signal_wifi_0_bar" color: Appearance.inirEverywhere ? Appearance.inir.colTextSecondary : Appearance.colors.colOnSurfaceVariant } - StyledText { + ColumnLayout { + spacing: 2 Layout.fillWidth: true - color: Appearance.inirEverywhere ? Appearance.inir.colText : Appearance.colors.colOnSurfaceVariant - elide: Text.ElideRight - text: root.wifiNetwork?.ssid ?? Translation.tr("Unknown") + StyledText { + Layout.fillWidth: true + color: Appearance.inirEverywhere ? Appearance.inir.colText : Appearance.colors.colOnSurfaceVariant + elide: Text.ElideRight + text: root.wifiNetwork?.ssid ?? Translation.tr("Unknown") + } + Revealer { + vertical: true + reveal: (root.wifiNetwork?.active || Network.wifiConnectTarget === root.wifiNetwork) ?? false + Layout.fillWidth: true + StyledText { + font.pixelSize: Appearance.font.pixelSize.smaller + color: Appearance.inirEverywhere ? Appearance.inir.colTextSecondary : Appearance.colors.colSubtext + elide: Text.ElideRight + text: { + if (!root.wifiNetwork) return ""; + if (Network.wifiConnectTarget === root.wifiNetwork && !root.wifiNetwork.active) { + return Translation.tr("Connecting…"); + } + if (!root.wifiNetwork.active) return ""; + let details = []; + details.push(Translation.tr("Connected")); + details.push(root.wifiNetwork.strength + "%"); + if (root.wifiNetwork.frequency) { + let ghz = root.wifiNetwork.frequency > 5900 ? "6 GHz" : (root.wifiNetwork.frequency > 4000 ? "5 GHz" : "2.4 GHz"); + details.push(ghz + " (" + root.wifiNetwork.frequency + " MHz)"); + } + if (root.wifiNetwork.rate) { + details.push(root.wifiNetwork.rate); + } + if (root.wifiNetwork.bssid) { + details.push(root.wifiNetwork.bssid); + } + return details.join(" • "); + } + } + } } MaterialSymbol { - visible: (root.wifiNetwork?.isSecure || root.wifiNetwork?.active) ?? false - text: root.wifiNetwork?.active ? "check" : Network.wifiConnectTarget === root.wifiNetwork ? "settings_ethernet" : "lock" + id: wifiStatusIcon + visible: (root.wifiNetwork?.isSecure || root.wifiNetwork?.active || Network.wifiConnectTarget === root.wifiNetwork) ?? false + text: root.wifiNetwork?.active ? "check" : Network.wifiConnectTarget === root.wifiNetwork ? "sync" : "lock" iconSize: Appearance.font.pixelSize.larger color: Appearance.inirEverywhere ? Appearance.inir.colTextSecondary : Appearance.colors.colOnSurfaceVariant + + RotationAnimator { + target: wifiStatusIcon + from: 0 + to: 360 + duration: 1500 + running: Network.wifiConnectTarget === root.wifiNetwork && !root.wifiNetwork?.active + loops: Animation.Infinite + } } } ColumnLayout { // Password id: passwordPrompt Layout.topMargin: 8 - visible: root.wifiNetwork?.askingPassword ?? false + visible: (root.wifiNetwork?.askingPassword && !root.wifiNetwork?.active) ?? false - MaterialTextField { - id: passwordField + RowLayout { Layout.fillWidth: true - placeholderText: Translation.tr("Password") + spacing: 8 - // Password - echoMode: TextInput.Password - inputMethodHints: Qt.ImhSensitiveData + MaterialTextField { + id: passwordField + Layout.fillWidth: true + placeholderText: Translation.tr("Password") - onAccepted: { - Network.changePassword(root.wifiNetwork, passwordField.text); + // Password + echoMode: showPasswordButton.showPassword ? TextInput.Normal : TextInput.Password + inputMethodHints: Qt.ImhSensitiveData + + onAccepted: { + Network.changePassword(root.wifiNetwork, passwordField.text); + } + } + + RippleButton { + id: showPasswordButton + property bool showPassword: false + Layout.preferredHeight: passwordField.implicitHeight + Layout.preferredWidth: passwordField.implicitHeight + buttonRadius: (passwordField.background && passwordField.background.radius !== undefined) ? passwordField.background.radius : 4 + colBackground: Appearance.colors.colLayer2 + onClicked: (mouse) => { + showPassword = !showPassword + // Stop the click from bubbling up to the parent + // DialogListItem, which would trigger connectToWifiNetwork() + if (mouse) mouse.accepted = true + } + + contentItem: MaterialSymbol { + anchors.centerIn: parent + text: showPasswordButton.showPassword ? "visibility" : "visibility_off" + iconSize: Appearance.font.pixelSize.larger + color: Appearance.colors.colOnSurfaceVariant + } } } diff --git a/modules/verticalBar/VerticalBarContent.qml b/modules/verticalBar/VerticalBarContent.qml index a8db7d76c..8350c6969 100644 --- a/modules/verticalBar/VerticalBarContent.qml +++ b/modules/verticalBar/VerticalBarContent.qml @@ -46,7 +46,7 @@ Item { // Bar content region anchorItem: barContextMenuAnchor popupSide: root.barOnRight ? Edges.Left : Edges.Right closeOnFocusLost: true - closeOnHoverLost: true + closeOnHoverLost: false model: [ { @@ -233,11 +233,11 @@ Item { // Bar content region onScrollDown: root.brightnessMonitor.setBrightness(root.brightnessMonitor.brightness - 0.05) onScrollUp: root.brightnessMonitor.setBrightness(root.brightnessMonitor.brightness + 0.05) onMovedAway: GlobalStates.osdBrightnessOpen = false - onPressed: event => { - if (event.button === Qt.LeftButton) + onReleased: mouse => { + if (mouse.button === Qt.LeftButton) GlobalStates.sidebarLeftOpen = !GlobalStates.sidebarLeftOpen; - else if (event.button === Qt.RightButton) - root.openBarContextMenu(event.x, event.y, barTopSectionMouseArea) + else if (mouse.button === Qt.RightButton) + root.openBarContextMenu(mouse.x, mouse.y, barTopSectionMouseArea) } ColumnLayout { // Content @@ -329,8 +329,8 @@ Item { // Bar content region anchors.fill: parent acceptedButtons: Qt.RightButton - onPressed: event => { - if (event.button === Qt.RightButton) { + onReleased: mouse => { + if (mouse.button === Qt.RightButton) { GlobalStates.overviewOpen = !GlobalStates.overviewOpen; } } @@ -415,11 +415,11 @@ Item { // Bar content region onScrollDown: Audio.decrementVolume(); onScrollUp: Audio.incrementVolume(); onMovedAway: GlobalStates.osdVolumeOpen = false; - onPressed: event => { - if (event.button === Qt.LeftButton) { + onReleased: mouse => { + if (mouse.button === Qt.LeftButton) { GlobalStates.sidebarRightOpen = !GlobalStates.sidebarRightOpen; - } else if (event.button === Qt.RightButton) { - root.openBarContextMenu(event.x, event.y, barBottomSectionMouseArea) + } else if (mouse.button === Qt.RightButton) { + root.openBarContextMenu(mouse.x, mouse.y, barBottomSectionMouseArea) } } @@ -464,7 +464,7 @@ Item { // Bar content region animation: ColorAnimation { duration: Appearance.animation.elementMoveFast.duration; easing.type: Appearance.animation.elementMoveFast.type; easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve } } - onPressed: { + onReleased: { GlobalStates.sidebarRightOpen = !GlobalStates.sidebarRightOpen; } diff --git a/modules/waffle/actionCenter/toggles/ActionCenterToggleButton.qml b/modules/waffle/actionCenter/toggles/ActionCenterToggleButton.qml index 39fb1ce91..57dac196e 100644 --- a/modules/waffle/actionCenter/toggles/ActionCenterToggleButton.qml +++ b/modules/waffle/actionCenter/toggles/ActionCenterToggleButton.qml @@ -126,4 +126,9 @@ ColumnLayout { } } } + + StyledToolTip { + visible: root.tooltipText !== "" + text: root.tooltipText + } } diff --git a/modules/waffle/actionCenter/wifi/WWifiNetworkItem.qml b/modules/waffle/actionCenter/wifi/WWifiNetworkItem.qml index 0e26e8645..52c15fe6c 100644 --- a/modules/waffle/actionCenter/wifi/WWifiNetworkItem.qml +++ b/modules/waffle/actionCenter/wifi/WWifiNetworkItem.qml @@ -66,7 +66,15 @@ ExpandableChoiceButton { id: statusText Layout.fillWidth: true elide: Text.ElideRight - text: root.wifiNetwork?.active ? Translation.tr("Connected") : root.wifiNetwork?.isSecure ? Translation.tr("Secured") : Translation.tr("Not secured") + text: { + if (root.wifiNetwork?.active) { + let info = Translation.tr("Connected"); + if (root.wifiNetwork.strength) info += " • " + root.wifiNetwork.strength + "%"; + if (root.wifiNetwork.rate) info += " • " + root.wifiNetwork.rate; + return info; + } + return root.wifiNetwork?.isSecure ? Translation.tr("Secured") : Translation.tr("Not secured"); + } font.pixelSize: Looks.font.pixelSize.large color: Looks.colors.subfg visible: root.wifiNetwork?.active || root.expanded diff --git a/modules/waffle/settings/pages/WGeneralPage.qml b/modules/waffle/settings/pages/WGeneralPage.qml index a096054ea..28838198f 100644 --- a/modules/waffle/settings/pages/WGeneralPage.qml +++ b/modules/waffle/settings/pages/WGeneralPage.qml @@ -32,7 +32,7 @@ WSettingsPage { label: Translation.tr("Maximum volume") icon: "speaker-1" suffix: "%" - from: 50; to: 150; stepSize: 5 + from: 50; to: 200; stepSize: 5 value: Config.options?.audio?.protection?.maxAllowed ?? 99 onValueChanged: Config.setNestedValue("audio.protection.maxAllowed", value) } diff --git a/scripts/inir b/scripts/inir index 7697a5b3c..baa508272 100755 --- a/scripts/inir +++ b/scripts/inir @@ -3008,17 +3008,31 @@ launch_configured_browser() { browser="${browser:-xdg-open}" if command -v "$browser" >/dev/null 2>&1; then - if [[ $# -gt 0 ]]; then - exec "$browser" "$@" + if command -v systemd-run >/dev/null 2>&1; then + if [[ $# -gt 0 ]]; then + exec systemd-run --user --scope --quiet --collect --property="Description=iNiR Browser" -- "$browser" "$@" + fi + exec systemd-run --user --scope --quiet --collect --property="Description=iNiR Browser" -- "$browser" + else + if [[ $# -gt 0 ]]; then + exec "$browser" "$@" + fi + exec "$browser" fi - exec "$browser" fi if command -v xdg-open >/dev/null 2>&1; then - if [[ $# -gt 0 ]]; then - exec xdg-open "$@" + if command -v systemd-run >/dev/null 2>&1; then + if [[ $# -gt 0 ]]; then + exec systemd-run --user --scope --quiet --collect --property="Description=XDG Open" -- xdg-open "$@" + fi + exec systemd-run --user --scope --quiet --collect --property="Description=XDG Open" -- xdg-open "https://" + else + if [[ $# -gt 0 ]]; then + exec xdg-open "$@" + fi + exec xdg-open "https://" fi - exec xdg-open "https://" fi echo "No browser launcher found" >&2 diff --git a/scripts/launch-terminal.sh b/scripts/launch-terminal.sh index 0f70fd2fa..65665d4dc 100755 --- a/scripts/launch-terminal.sh +++ b/scripts/launch-terminal.sh @@ -17,13 +17,21 @@ fi TERMINAL="${TERMINAL:-kitty}" if command -v "$TERMINAL" &>/dev/null; then - exec "$TERMINAL" "$@" + if command -v systemd-run &>/dev/null; then + exec systemd-run --user --scope --quiet --collect --property="Description=iNiR Terminal" -- "$TERMINAL" "$@" + else + exec "$TERMINAL" "$@" + fi fi # Fallback chain: project default first, then popular alternatives for fallback in kitty foot ghostty alacritty wezterm konsole xterm; do if command -v "$fallback" &>/dev/null; then - exec "$fallback" "$@" + if command -v systemd-run &>/dev/null; then + exec systemd-run --user --scope --quiet --collect --property="Description=iNiR Terminal" -- "$fallback" "$@" + else + exec "$fallback" "$@" + fi fi done diff --git a/services/Audio.qml b/services/Audio.qml index 60df63a4c..ea60cb6fc 100644 --- a/services/Audio.qml +++ b/services/Audio.qml @@ -273,22 +273,26 @@ Singleton { } function incrementVolume() { - // Fire wpctl relative increment first — works even when sink?.audio is not yet tracked. - if (!wpctlIncrementSinkVolume.running) - wpctlIncrementSinkVolume.running = true - if (!root.sink?.audio) return; + // Fire wpctl relative increment only when sink?.audio is not yet tracked. + if (!root.sink?.audio) { + if (!wpctlIncrementSinkVolume.running) + wpctlIncrementSinkVolume.running = true + return; + } const currentVolume = root.sink.audio.volume; - const step = currentVolume < 0.1 ? 0.01 : 0.02; + const step = 0.02; root.sink.audio.volume = Math.min(root.hardMaxValue, currentVolume + step); } function decrementVolume() { - // Fire wpctl relative decrement first — works even when sink?.audio is not yet tracked. - if (!wpctlDecrementSinkVolume.running) - wpctlDecrementSinkVolume.running = true - if (!root.sink?.audio) return; + // Fire wpctl relative decrement only when sink?.audio is not yet tracked. + if (!root.sink?.audio) { + if (!wpctlDecrementSinkVolume.running) + wpctlDecrementSinkVolume.running = true + return; + } const currentVolume = root.sink.audio.volume; - const step = currentVolume <= 0.1 ? 0.01 : 0.02; + const step = 0.02; root.sink.audio.volume = Math.max(0, currentVolume - step); } diff --git a/services/AutoPip.qml b/services/AutoPip.qml new file mode 100644 index 000000000..115461b5f --- /dev/null +++ b/services/AutoPip.qml @@ -0,0 +1,250 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.services +import qs.modules.common + +Singleton { + id: root + + // Monitor changes to Niri windows and active window + readonly property var activeWindow: { + const wins = NiriService.windows || []; + for (let i = 0; i < wins.length; i++) { + if (wins[i] && wins[i].is_focused) return wins[i]; + } + return null; + } + readonly property var windows: NiriService.windows + + // Check if Firefox/Zen is playing media via MprisController + readonly property bool isFirefoxPlaying: { + const players = MprisController.players || []; + for (let i = 0; i < players.length; i++) { + const player = players[i]; + if (!player) continue; + const identity = (player.identity ?? "").toLowerCase(); + if ((identity.includes("firefox") || identity.includes("zen")) && player.isPlaying) { + return true; + } + } + return false; + } + + // Grace period property to smooth out asynchronous D-Bus/MPRIS status lags + property bool wasFirefoxPlayingRecently: false + + Timer { + id: playingGraceTimer + interval: 1000 + repeat: false + onTriggered: wasFirefoxPlayingRecently = false + } + + onIsFirefoxPlayingChanged: { + if (isFirefoxPlaying) { + playingGraceTimer.stop(); + wasFirefoxPlayingRecently = true; + } else { + playingGraceTimer.restart(); + } + } + + // Keep track of the active window ID + property string lastActiveWindowId: "" + + // Computed property that dynamically checks if a Firefox PiP window actually exists in Niri + readonly property bool isPipActive: { + const wins = windows || []; + for (let i = 0; i < wins.length; i++) { + const w = wins[i]; + if (!w) continue; + const app = (w.app_id ?? "").toLowerCase(); + const title = (w.title ?? "").toLowerCase(); + if ((app.includes("firefox") || app.includes("zen")) && + (title === "picture-in-picture" || title === "incrustation" || title === "bild-in-bild" || title.includes("picture-in-picture"))) { + return true; + } + } + return false; + } + + // Timer to debounce PiP triggers and prevent keypress injection spam during rapid scrolling + Timer { + id: pipTriggerDebounce + interval: 150 + repeat: false + property string targetWinId: "" + onTriggered: { + if (targetWinId === "") return; + const firefoxWin = windows.find(w => String(w.id) === targetWinId); + if (firefoxWin) { + if (firefoxWin.is_floating) return; + const offScreen = isWindowOffScreen(firefoxWin); + console.log("[AutoPip Debounced] Check. playing:", wasFirefoxPlayingRecently, "offScreen:", offScreen); + if (wasFirefoxPlayingRecently && offScreen) { + console.log("[AutoPip Debounced] Firefox is off-screen. Triggering PiP..."); + triggerPipToggle(); + } + } + targetWinId = ""; + } + } + + Timer { + id: debugTimer + interval: 2000 + running: true + repeat: true + onTriggered: { + console.log("[AutoPip Poll] activeWindow:", activeWindow ? activeWindow.title : "null", + "app_id:", activeWindow ? activeWindow.app_id : "null", + "windows count:", windows ? windows.length : 0); + if (windows) { + for (let i = 0; i < windows.length; i++) { + const w = windows[i]; + console.log(" -> Window id:", w.id, "app_id:", w.app_id, "is_focused:", w.is_focused, "title:", w.title); + } + } + } + } + + onActiveWindowChanged: { + console.log("[AutoPip] Active window changed:", activeWindow ? activeWindow.title : "null", "app_id:", activeWindow ? activeWindow.app_id : "null"); + + // Cancel any pending trigger when focus changes (prevents keypress injection while passing by columns) + pipTriggerDebounce.stop(); + pipTriggerDebounce.targetWinId = ""; + + if (!activeWindow) return; + + // If the newly focused window is Firefox, check if we need to dock it back + const appName = (activeWindow.app_id ?? "").toLowerCase(); + const isFirefox = appName.includes("firefox") || appName.includes("zen"); + console.log("[AutoPip] isFirefox:", isFirefox, "lastActiveWindowId:", lastActiveWindowId, "isPipActive:", isPipActive, "wasFirefoxPlayingRecently:", wasFirefoxPlayingRecently); + + if (isFirefox) { + // Ignore if the newly focused window is the PiP window itself + const title = (activeWindow.title ?? "").toLowerCase(); + if (title === "picture-in-picture" || title === "incrustation" || title === "bild-in-bild" || title.includes("picture-in-picture")) { + console.log("[AutoPip] Focused window is the PiP window itself. Skip docking."); + return; + } + + if (isPipActive) { + // Dock back: send keypress to dock video back + console.log("[AutoPip] Firefox focused and PiP is active, docking back..."); + triggerPipToggle(); + } + lastActiveWindowId = String(activeWindow.id); + return; + } + + // If focus shifted away from Firefox: + // Schedule a debounced check to allow focus to settle (prevents collision during fast scroll) + if (lastActiveWindowId !== "") { + pipTriggerDebounce.targetWinId = lastActiveWindowId; + pipTriggerDebounce.start(); + } + + lastActiveWindowId = ""; + } + + function isWindowOffScreen(win): bool { + // Find the active workspace + const wsId = win.workspace_id; + const ws = NiriService.workspaces[wsId]; + if (!ws) { + console.log("[AutoPip] Workspace not found for window, wsId:", wsId); + return true; + } + + // 1. Get the screen geometry dynamically + const outputName = ws.output || NiriService.currentOutput; + if (!outputName || !NiriService.outputs[outputName]) { + console.log("[AutoPip] Output not found for name:", outputName); + return true; + } + const output = NiriService.outputs[outputName]; + const screenWidth = output.logical?.width || 1920; + + // 2. Filter and sort all tiled windows on this workspace + const wsWindows = windows.filter(w => w.workspace_id === wsId && !w.is_floating); + wsWindows.sort((a, b) => { + const colA = a.layout?.pos_in_scrolling_layout?.[0] ?? 0; + const colB = b.layout?.pos_in_scrolling_layout?.[0] ?? 0; + return colA - colB; + }); + + if (wsWindows.length === 0) { + console.log("[AutoPip] No tiled windows found on workspace:", wsId); + return true; + } + + // 3. Compute absolute positions of all columns on the ribbon + const gaps = 10; // Niri gaps size config + const colWidths = {}; + const colAbsX = {}; + + for (let i = 0; i < wsWindows.length; i++) { + const w = wsWindows[i]; + const colIdx = w.layout?.pos_in_scrolling_layout?.[0] ?? 0; + if (colWidths[colIdx] === undefined) { + colWidths[colIdx] = w.layout?.tile_size?.[0] ?? 945; + } + } + + const uniqueCols = Object.keys(colWidths).map(Number).sort((a, b) => a - b); + let currentX = 0; + for (let i = 0; i < uniqueCols.length; i++) { + const col = uniqueCols[i]; + colAbsX[col] = currentX; + currentX += colWidths[col] + gaps; + } + + // 4. Find the focused window on the active workspace + // Use the newly active window directly to avoid race conditions with the windows list updates. + const focusedWin = activeWindow; + if (!focusedWin) { + console.log("[AutoPip] Focused window not found (activeWindow is null)"); + return true; + } + + const focusedCol = focusedWin.layout?.pos_in_scrolling_layout?.[0] ?? 0; + const focusedWidth = colWidths[focusedCol] ?? 945; + const focusedAbsX = colAbsX[focusedCol] ?? 0; + + // Check window visibility in the two potential alignment viewports + const winCol = win.layout?.pos_in_scrolling_layout?.[0] ?? 0; + const winWidth = colWidths[winCol] ?? 945; + const winAbsX = colAbsX[winCol] ?? 0; + + // Case A: left-aligned focused column + const vX_A = focusedAbsX; + const relX_A = winAbsX - vX_A; + // Apply 50px tolerance to ignore margins/gaps at screen edges + const visible_A = (relX_A + winWidth > 50) && (relX_A < screenWidth - 50); + + // Case B: right-aligned focused column + const vX_B = focusedAbsX + focusedWidth - screenWidth; + const relX_B = winAbsX - vX_B; + const visible_B = (relX_B + winWidth > 50) && (relX_B < screenWidth - 50); + + console.log("[AutoPip] Geometry Details:", + "screenWidth:", screenWidth, + "focusedCol:", focusedCol, "focusedAbsX:", focusedAbsX, "focusedWidth:", focusedWidth, + "winCol:", winCol, "winAbsX:", winAbsX, "winWidth:", winWidth, + "relX_A:", relX_A, "visible_A:", visible_A, + "relX_B:", relX_B, "visible_B:", visible_B); + + return !visible_A && !visible_B; + } + + function triggerPipToggle() { + console.log("[AutoPip] triggerPipToggle called (wtype simulation disabled for stability)."); + // Disabled to prevent sticky modifier keys (Ctrl/Shift) under Wayland Niri transitions + // Quickshell.execDetached(["wtype", "-M", "ctrl", "-M", "shift", "-k", "bracketright"]); + } +} diff --git a/services/Brightness.qml b/services/Brightness.qml index c7bb6b9e1..d3f39b8df 100644 --- a/services/Brightness.qml +++ b/services/Brightness.qml @@ -78,6 +78,7 @@ Singleton { id: monitor required property ShellScreen screen + property string deviceName: "" readonly property bool isDdc: { const match = root.ddcMonitors.find(m => screen.model?.includes(m.model) && !root.monitors.slice(0, root.monitors.indexOf(this)).some(mon => mon.busNum === m.busNum)); return !!match; @@ -113,17 +114,26 @@ Singleton { function initialize() { monitor.ready = false; - initProc.command = isDdc ? ["ddcutil", "-b", busNum, "getvcp", "10", "--brief"] : ["sh", "-c", `echo "a b c $(brightnessctl g) $(brightnessctl m)"`]; + initProc.command = isDdc ? ["ddcutil", "-b", busNum, "getvcp", "10", "--brief"] : ["sh", "-c", `dev=$(ls -1 /sys/class/backlight/ | grep -E 'amdgpu|intel|nouveau' | head -n 1); [ -z "$dev" ] && dev=$(ls -1 /sys/class/backlight/ | head -n 1); echo "a b c $(brightnessctl -d "$dev" g) $(brightnessctl -d "$dev" m) $dev"`]; initProc.running = true; } readonly property Process initProc: Process { - stdout: SplitParser { - onRead: data => { - const [, , , current, max] = data.split(" "); - monitor.rawMaxBrightness = parseInt(max); - monitor.brightness = parseInt(current) / monitor.rawMaxBrightness; - monitor.ready = true; + stdout: StdioCollector { + onStreamFinished: { + const parts = text.trim().split(" "); + if (parts.length >= 5) { + if (parts.length >= 6) { + monitor.deviceName = parts[5]; + } + const maxVal = parseInt(parts[4]); + const curVal = parseInt(parts[3]); + if (!isNaN(maxVal) && !isNaN(curVal) && maxVal > 0) { + monitor.rawMaxBrightness = maxVal; + monitor.brightness = curVal / maxVal; + monitor.ready = true; + } + } } } } @@ -140,7 +150,7 @@ Singleton { function syncBrightness() { const brightnessValue = Math.max(monitor.multipliedBrightness, 0) const rawValueRounded = Math.max(Math.floor(brightnessValue * monitor.rawMaxBrightness), 1); - setProc.command = isDdc ? ["ddcutil", "-b", busNum, "setvcp", "10", rawValueRounded] : ["brightnessctl", "--class", "backlight", "s", rawValueRounded, "--quiet"]; + setProc.command = isDdc ? ["ddcutil", "-b", busNum, "setvcp", "10", rawValueRounded] : (monitor.deviceName ? ["brightnessctl", "-d", monitor.deviceName, "s", rawValueRounded, "--quiet"] : ["brightnessctl", "--class", "backlight", "s", rawValueRounded, "--quiet"]); setProc.startDetached(); } diff --git a/services/Network.qml b/services/Network.qml index c664df141..f184ca0d4 100644 --- a/services/Network.qml +++ b/services/Network.qml @@ -29,7 +29,7 @@ Singleton { property int networkStrength property string materialSymbol: root.ethernet ? "lan" - : root.wifiEnabled + : root.wifi ? ( Network.networkStrength > 83 ? "signal_wifi_4_bar" : Network.networkStrength > 67 ? "network_wifi" : @@ -64,15 +64,24 @@ Singleton { function connectToWifiNetwork(accessPoint: WifiAccessPoint): void { accessPoint.askingPassword = false; root.wifiConnectTarget = accessPoint; - // We use this instead of `nmcli connection up SSID` because this also creates a connection profile - connectProc.exec(["nmcli", "dev", "wifi", "connect", accessPoint.ssid]) - + // Connect to existing profile if it exists, otherwise create a new one + connectProc.exec([ + "sh", "-c", + 'if nmcli -t -f NAME,TYPE connection show | grep -Fxq "$1:802-11-wireless"; then nmcli connection up "$1"; else nmcli dev wifi connect "$1"; fi', + "--", accessPoint.ssid + ]) } function disconnectWifiNetwork(): void { if (active) disconnectProc.exec(["nmcli", "connection", "down", active.ssid]); } + function refreshActiveNetworkDetails(): void { + if (!getNetworks.running) { + getNetworks.running = true; + } + } + function openPublicWifiPortal() { Quickshell.execDetached(["xdg-open", "https://nmcheck.gnome.org/"]) // From some StackExchange thread, seems to work } @@ -91,6 +100,7 @@ Singleton { Process { id: connectProc + property bool secretsRequired: false environment: ({ LANG: "C", LC_ALL: "C" @@ -98,27 +108,34 @@ Singleton { stdout: SplitParser { onRead: line => { // print(line) - getNetworks.running = true + if (!getNetworks.running) + getNetworks.running = true } } stderr: SplitParser { onRead: line => { // print("err:", line) if (line.includes("Secrets were required")) { - root.wifiConnectTarget.askingPassword = true + connectProc.secretsRequired = true } } } onExited: (exitCode, exitStatus) => { - root.wifiConnectTarget.askingPassword = (exitCode !== 0) + if (root.wifiConnectTarget) { + root.wifiConnectTarget.askingPassword = connectProc.secretsRequired + } root.wifiConnectTarget = null + connectProc.secretsRequired = false } } Process { id: disconnectProc stdout: SplitParser { - onRead: getNetworks.running = true + onRead: { + if (!getNetworks.running) + getNetworks.running = true; + } } } @@ -136,7 +153,8 @@ Singleton { stdout: SplitParser { onRead: { wifiScanning = false; - getNetworks.running = true; + if (!getNetworks.running) + getNetworks.running = true; } } } @@ -149,6 +167,19 @@ Singleton { onTriggered: root._doUpdate() } + // Periodic connectivity re-check (every 30 s). + // NM's built-in connectivity check runs on its own interval, but this + // ensures the shell icon reflects the latest state even if nmcli monitor + // misses a quiet state transition (e.g. AP drops without sending a + // deauth frame, or NM connectivity flips between "limited" and "full"). + Timer { + id: _periodicCheck + interval: 30000 + repeat: true + running: false + onTriggered: root.update() + } + // Status update (debounced — nmcli monitor can emit rapid bursts) function update() { _updateDebounce.restart(); @@ -156,10 +187,14 @@ Singleton { // Actual update logic function _doUpdate() { - updateConnectionType.startCheck(); - wifiStatusProcess.running = true - updateNetworkName.running = true; - updateNetworkStrength.running = true; + if (!updateConnectionType.running) + updateConnectionType.startCheck(); + if (!wifiStatusProcess.running) + wifiStatusProcess.running = true; + if (!updateNetworkName.running) + updateNetworkName.running = true; + if (!updateNetworkStrength.running) + updateNetworkStrength.running = true; } property bool _destroying: false @@ -227,7 +262,7 @@ Singleton { wifiStatus = "connected" if (connectivity === "limited") { - hasWifi = false; + hasWifi = true; wifiStatus = "limited" } } @@ -247,11 +282,11 @@ Singleton { Process { id: updateNetworkName - command: ["sh", "-c", "nmcli -t -f NAME c show --active | head -1"] + command: ["sh", "-c", "nmcli -t -f TYPE,NAME c show --active | awk -F: '$1 == \"802-11-wireless\" || $1 == \"802-3-ethernet\" {print $2; exit}'"] running: false stdout: SplitParser { onRead: data => { - root.networkName = data; + root.networkName = data.trim(); } } } @@ -259,10 +294,11 @@ Singleton { Process { id: updateNetworkStrength running: false - command: ["sh", "-c", "nmcli -f IN-USE,SIGNAL,SSID device wifi | awk '/^\\*/{if (NR!=1) {print $2}}'"] + command: ["sh", "-c", "nmcli -t -f ACTIVE,SIGNAL dev wifi | grep '^yes:' | cut -d: -f2"] stdout: SplitParser { onRead: data => { - root.networkStrength = parseInt(data); + const val = parseInt(data.trim()); + root.networkStrength = isNaN(val) ? 0 : val; } } } @@ -285,7 +321,7 @@ Singleton { Process { id: getNetworks running: false - command: ["nmcli", "-g", "ACTIVE,SIGNAL,FREQ,SSID,BSSID,SECURITY", "d", "w"] + command: ["nmcli", "-g", "ACTIVE,SIGNAL,FREQ,SSID,BSSID,SECURITY,RATE", "d", "w"] environment: ({ LANG: "C", LC_ALL: "C" @@ -304,7 +340,8 @@ Singleton { frequency: parseInt(net[2]), ssid: net[3], bssid: net[4]?.replace(rep2, ":") ?? "", - security: net[5] || "" + security: net[5] || "", + rate: net[6] || "" }; }).filter(n => n.ssid && n.ssid.length > 0); diff --git a/services/TrayService.qml b/services/TrayService.qml index ac029e8e7..cc426681f 100644 --- a/services/TrayService.qml +++ b/services/TrayService.qml @@ -151,16 +151,30 @@ Singleton { return false; } - // Filter out invalid items (null or missing id) function isValidItem(item) { return item && item.id; } + function isIgnored(item) { + if (!isValidItem(item)) return true; + const id = (item.id || "").toLowerCase(); + const title = (item.title || "").toLowerCase(); + const tooltip = (item.tooltipTitle || "").toLowerCase(); + const ignored = Config.options?.bar?.tray?.ignoredItems + ?? ["nm-applet", "network-manager-applet", "networkmanager-applet"]; + for (let i = 0; i < ignored.length; i++) { + const pattern = ignored[i].toLowerCase(); + if (id.indexOf(pattern) !== -1 || title.indexOf(pattern) !== -1 || tooltip.indexOf(pattern) !== -1) { + return true; + } + } + return false; + } + property var _pinnedItems: Config.options?.tray?.pinnedItems ?? [] - property list itemsInUserList: SystemTray.items.values.filter(i => (isValidItem(i) && _pinnedItems.includes(i.id))) - property list itemsNotInUserList: SystemTray.items.values.filter(i => (isValidItem(i) && !_pinnedItems.includes(i.id) && (!smartTray || i.status !== Status.Passive))) - + property list itemsInUserList: SystemTray.items.values.filter(i => (isValidItem(i) && !isIgnored(i) && _pinnedItems.includes(i.id))) + property list itemsNotInUserList: SystemTray.items.values.filter(i => (isValidItem(i) && !isIgnored(i) && !_pinnedItems.includes(i.id) && (!smartTray || i.status !== Status.Passive))) property bool invertPins: Config.options?.tray?.invertPinnedItems ?? false property list pinnedItems: invertPins ? itemsNotInUserList : itemsInUserList property list unpinnedItems: invertPins ? itemsInUserList : itemsNotInUserList diff --git a/services/network/WifiAccessPoint.qml b/services/network/WifiAccessPoint.qml index 55ee811f5..3065cf558 100644 --- a/services/network/WifiAccessPoint.qml +++ b/services/network/WifiAccessPoint.qml @@ -9,6 +9,7 @@ QtObject { readonly property bool active: lastIpcObject.active readonly property string security: lastIpcObject.security readonly property bool isSecure: security.length > 0 + readonly property string rate: lastIpcObject.rate ?? "" property bool askingPassword: false } diff --git a/services/qmldir b/services/qmldir index eb8b41ad0..2300b72c6 100644 --- a/services/qmldir +++ b/services/qmldir @@ -60,3 +60,4 @@ singleton Weather 1.0 Weather.qml singleton WidgetPowerManager 1.0 WidgetPowerManager.qml singleton WindowPreviewService 1.0 WindowPreviewService.qml singleton YtMusic 1.0 YtMusic.qml +singleton AutoPip 1.0 AutoPip.qml diff --git a/shell.qml b/shell.qml index d130736e5..c14388ada 100644 --- a/shell.qml +++ b/shell.qml @@ -35,6 +35,7 @@ ShellRoot { // Force singleton instantiation — startup-critical only property var _idleService: Idle property var _powerProfilePersistence: PowerProfilePersistence + property var _autoPipService: AutoPip // Deferred singletons — initialized after first frame to reduce boot contention // Tier 3: T+500ms (display/interaction services)