diff options
Diffstat (limited to '')
70 files changed, 1097 insertions, 1677 deletions
diff --git a/assets/pam.d/fprint b/assets/pam.d/fprint deleted file mode 100644 index d4814e9..0000000 --- a/assets/pam.d/fprint +++ /dev/null @@ -1,3 +0,0 @@ -#%PAM-1.0 - -auth required pam_fprintd.so max-tries=1 diff --git a/assets/pam.d/passwd b/assets/pam.d/passwd deleted file mode 100644 index 4b14064..0000000 --- a/assets/pam.d/passwd +++ /dev/null @@ -1,6 +0,0 @@ -#%PAM-1.0 - -auth required pam_faillock.so preauth -auth [success=1 default=bad] pam_unix.so nullok -auth [default=die] pam_faillock.so authfail -auth required pam_faillock.so authsucc diff --git a/components/controls/SplitButtonRow.qml b/components/controls/SplitButtonRow.qml new file mode 100644 index 0000000..4ecc8bf --- /dev/null +++ b/components/controls/SplitButtonRow.qml @@ -0,0 +1,62 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.components +import qs.components.effects +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +StyledRect { + id: root + + required property string label + property int expandedZ: 100 + property bool enabled: true + + property alias menuItems: splitButton.menuItems + property alias active: splitButton.active + property alias expanded: splitButton.expanded + property alias type: splitButton.type + + signal selected(item: MenuItem) + + Layout.fillWidth: true + implicitHeight: row.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.layer(Colours.palette.m3surfaceContainer, 2) + + clip: false + z: splitButton.menu.implicitHeight > 0 ? expandedZ : 1 + opacity: enabled ? 1.0 : 0.5 + + RowLayout { + id: row + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: root.label + color: root.enabled ? Colours.palette.m3onSurface : Colours.palette.m3onSurfaceVariant + } + + SplitButton { + id: splitButton + enabled: root.enabled + type: SplitButton.Filled + + menu.z: 1 + + stateLayer.onClicked: { + splitButton.expanded = !splitButton.expanded + } + + menu.onItemSelected: (item) => { + root.selected(item); + } + } + } +} diff --git a/components/controls/ToggleButton.qml b/components/controls/ToggleButton.qml index b2c2afe..8bdcb04 100644 --- a/components/controls/ToggleButton.qml +++ b/components/controls/ToggleButton.qml @@ -74,7 +74,6 @@ StyledRect { } Loader { - asynchronous: true active: !!root.label visible: active @@ -103,7 +102,6 @@ StyledRect { Loader { id: tooltipLoader active: root.tooltip !== "" - asynchronous: true z: 10000 width: 0 height: 0 diff --git a/components/filedialog/FolderContents.qml b/components/filedialog/FolderContents.qml new file mode 100644 index 0000000..e16c7a1 --- /dev/null +++ b/components/filedialog/FolderContents.qml @@ -0,0 +1,228 @@ +pragma ComponentBehavior: Bound + +import ".." +import "../controls" +import "../images" +import qs.services +import qs.config +import qs.utils +import Caelestia.Models +import Quickshell +import QtQuick +import QtQuick.Layouts +import QtQuick.Effects + +Item { + id: root + + required property var dialog + property alias currentItem: view.currentItem + + StyledRect { + anchors.fill: parent + color: Colours.tPalette.m3surfaceContainer + + layer.enabled: true + layer.effect: MultiEffect { + maskSource: mask + maskEnabled: true + maskInverted: true + maskThresholdMin: 0.5 + maskSpreadAtMin: 1 + } + } + + Item { + id: mask + + anchors.fill: parent + layer.enabled: true + visible: false + + Rectangle { + anchors.fill: parent + anchors.margins: Appearance.padding.small + radius: Appearance.rounding.small + } + } + + Loader { + anchors.centerIn: parent + + opacity: view.count === 0 ? 1 : 0 + active: opacity > 0 + + sourceComponent: ColumnLayout { + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: "scan_delete" + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.extraLarge * 2 + font.weight: 500 + } + + StyledText { + text: qsTr("This folder is empty") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + } + + Behavior on opacity { + Anim {} + } + } + + GridView { + id: view + + anchors.fill: parent + anchors.margins: Appearance.padding.small + Appearance.padding.normal + + cellWidth: Sizes.itemWidth + Appearance.spacing.small + cellHeight: Sizes.itemWidth + Appearance.spacing.small * 2 + Appearance.padding.normal * 2 + 1 + + clip: true + focus: true + currentIndex: -1 + Keys.onEscapePressed: currentIndex = -1 + + Keys.onReturnPressed: { + if (root.dialog.selectionValid) + root.dialog.accepted(currentItem.modelData.path); + } + Keys.onEnterPressed: { + if (root.dialog.selectionValid) + root.dialog.accepted(currentItem.modelData.path); + } + + StyledScrollBar.vertical: StyledScrollBar { + flickable: view + } + + model: FileSystemModel { + path: { + if (root.dialog.cwd[0] === "Home") + return `${Paths.home}/${root.dialog.cwd.slice(1).join("/")}`; + else + return root.dialog.cwd.join("/"); + } + onPathChanged: view.currentIndex = -1 + } + + delegate: StyledRect { + id: item + + required property int index + required property FileSystemEntry modelData + + readonly property real nonAnimHeight: icon.implicitHeight + name.anchors.topMargin + name.implicitHeight + Appearance.padding.normal * 2 + + implicitWidth: Sizes.itemWidth + implicitHeight: nonAnimHeight + + radius: Appearance.rounding.normal + color: Qt.alpha(Colours.tPalette.m3surfaceContainerHighest, GridView.isCurrentItem ? Colours.tPalette.m3surfaceContainerHighest.a : 0) + z: GridView.isCurrentItem || implicitHeight !== nonAnimHeight ? 1 : 0 + clip: true + + StateLayer { + onDoubleClicked: { + if (item.modelData.isDir) + root.dialog.cwd.push(item.modelData.name); + else if (root.dialog.selectionValid) + root.dialog.accepted(item.modelData.path); + } + + function onClicked(): void { + view.currentIndex = item.index; + } + } + + CachingIconImage { + id: icon + + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: Appearance.padding.normal + + implicitSize: Sizes.itemWidth - Appearance.padding.normal * 2 + + Component.onCompleted: { + const file = item.modelData; + if (file.isImage) + source = Qt.resolvedUrl(file.path); + else if (!file.isDir) + source = Quickshell.iconPath(file.mimeType.replace("/", "-"), "application-x-zerosize"); + else if (root.dialog.cwd.length === 1 && ["Desktop", "Documents", "Downloads", "Music", "Pictures", "Public", "Templates", "Videos"].includes(file.name)) + source = Quickshell.iconPath(`folder-${file.name.toLowerCase()}`); + else + source = Quickshell.iconPath("inode-directory"); + } + } + + StyledText { + id: name + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: icon.bottom + anchors.topMargin: Appearance.spacing.small + anchors.margins: Appearance.padding.normal + + horizontalAlignment: Text.AlignHCenter + elide: item.GridView.isCurrentItem ? Text.ElideNone : Text.ElideRight + wrapMode: item.GridView.isCurrentItem ? Text.WrapAtWordBoundaryOrAnywhere : Text.NoWrap + + Component.onCompleted: text = item.modelData.name + } + + Behavior on implicitHeight { + Anim {} + } + } + + add: Transition { + Anim { + properties: "opacity,scale" + from: 0 + to: 1 + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } + + remove: Transition { + Anim { + property: "opacity" + to: 0 + } + Anim { + property: "scale" + to: 0.5 + } + } + + displaced: Transition { + Anim { + properties: "opacity,scale" + to: 1 + easing.bezierCurve: Appearance.anim.curves.standardDecel + } + Anim { + properties: "x,y" + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } + } + + CurrentItem { + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: Appearance.padding.small + + currentItem: view.currentItem + } +} diff --git a/components/filedialog/HeaderBar.qml b/components/filedialog/HeaderBar.qml new file mode 100644 index 0000000..c9a3feb --- /dev/null +++ b/components/filedialog/HeaderBar.qml @@ -0,0 +1,139 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +StyledRect { + id: root + + required property var dialog + + implicitWidth: inner.implicitWidth + Appearance.padding.normal * 2 + implicitHeight: inner.implicitHeight + Appearance.padding.normal * 2 + + color: Colours.tPalette.m3surfaceContainer + + RowLayout { + id: inner + + anchors.fill: parent + anchors.margins: Appearance.padding.normal + spacing: Appearance.spacing.small + + Item { + implicitWidth: implicitHeight + implicitHeight: upIcon.implicitHeight + Appearance.padding.small * 2 + + StateLayer { + radius: Appearance.rounding.small + disabled: root.dialog.cwd.length === 1 + + function onClicked(): void { + root.dialog.cwd.pop(); + } + } + + MaterialIcon { + id: upIcon + + anchors.centerIn: parent + text: "drive_folder_upload" + color: root.dialog.cwd.length === 1 ? Colours.palette.m3outline : Colours.palette.m3onSurface + grade: 200 + } + } + + StyledRect { + Layout.fillWidth: true + + radius: Appearance.rounding.small + color: Colours.tPalette.m3surfaceContainerHigh + + implicitHeight: pathComponents.implicitHeight + pathComponents.anchors.margins * 2 + + RowLayout { + id: pathComponents + + anchors.fill: parent + anchors.margins: Appearance.padding.small / 2 + anchors.leftMargin: 0 + + spacing: Appearance.spacing.small + + Repeater { + model: root.dialog.cwd + + RowLayout { + id: folder + + required property string modelData + required property int index + + spacing: 0 + + Loader { + Layout.rightMargin: Appearance.spacing.small + active: folder.index > 0 + sourceComponent: StyledText { + text: "/" + color: Colours.palette.m3onSurfaceVariant + font.bold: true + } + } + + Item { + implicitWidth: homeIcon.implicitWidth + (homeIcon.active ? Appearance.padding.small : 0) + folderName.implicitWidth + Appearance.padding.normal * 2 + implicitHeight: folderName.implicitHeight + Appearance.padding.small * 2 + + Loader { + anchors.fill: parent + active: folder.index < root.dialog.cwd.length - 1 + sourceComponent: StateLayer { + radius: Appearance.rounding.small + + function onClicked(): void { + root.dialog.cwd = root.dialog.cwd.slice(0, folder.index + 1); + } + } + } + + Loader { + id: homeIcon + + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Appearance.padding.normal + + active: folder.index === 0 && folder.modelData === "Home" + sourceComponent: MaterialIcon { + text: "home" + color: root.dialog.cwd.length === 1 ? Colours.palette.m3onSurface : Colours.palette.m3onSurfaceVariant + fill: 1 + } + } + + StyledText { + id: folderName + + anchors.left: homeIcon.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: homeIcon.active ? Appearance.padding.small : 0 + + text: folder.modelData + color: folder.index < root.dialog.cwd.length - 1 ? Colours.palette.m3onSurfaceVariant : Colours.palette.m3onSurface + font.bold: true + } + } + } + } + + Item { + Layout.fillWidth: true + } + } + } + } +} diff --git a/config/BarConfig.qml b/config/BarConfig.qml index 00df1ea..c9c9f1f 100644 --- a/config/BarConfig.qml +++ b/config/BarConfig.qml @@ -85,6 +85,7 @@ JsonObject { property bool showMicrophone: false property bool showKbLayout: false property bool showNetwork: true + property bool showWifi: true property bool showBluetooth: true property bool showBattery: true property bool showLockStatus: true @@ -100,5 +101,6 @@ JsonObject { property int trayMenuWidth: 300 property int batteryWidth: 250 property int networkWidth: 320 + property int kbLayoutWidth: 320 } } diff --git a/config/Config.qml b/config/Config.qml index 7a5c04f..9f66c73 100644 --- a/config/Config.qml +++ b/config/Config.qml @@ -21,61 +21,11 @@ Singleton { property alias osd: adapter.osd property alias session: adapter.session property alias winfo: adapter.winfo - property alias lock: adapter.lock property alias utilities: adapter.utilities property alias sidebar: adapter.sidebar property alias services: adapter.services property alias paths: adapter.paths - // Public save function - call this to persist config changes - function save(): void { - saveTimer.restart(); - recentlySaved = true; - recentSaveCooldown.restart(); - } - - property bool recentlySaved: false - - ElapsedTimer { - id: timer - } - - Timer { - id: saveTimer - - interval: 500 - onTriggered: { - timer.restart(); - try { - // Parse current config to preserve structure and comments if possible - let config = {}; - try { - config = JSON.parse(fileView.text()); - } catch (e) { - // If parsing fails, start with empty object - config = {}; - } - - // Update config with current values - config = serializeConfig(); - - // Save to file with pretty printing - fileView.setText(JSON.stringify(config, null, 2)); - } catch (e) { - Toaster.toast(qsTr("Failed to serialize config"), e.message, "settings_alert", Toast.Error); - } - } - } - - Timer { - id: recentSaveCooldown - - interval: 2000 - onTriggered: { - recentlySaved = false; - } - } - // Helper function to serialize the config object function serializeConfig(): var { return { @@ -91,7 +41,6 @@ Singleton { osd: serializeOsd(), session: serializeSession(), winfo: serializeWinfo(), - lock: serializeLock(), utilities: serializeUtilities(), sidebar: serializeSidebar(), services: serializeServices(), @@ -126,17 +75,13 @@ Singleton { function serializeGeneral(): var { return { + logo: general.logo, apps: { terminal: general.apps.terminal, audio: general.apps.audio, playback: general.apps.playback, explorer: general.apps.explorer }, - idle: { - lockBeforeSleep: general.idle.lockBeforeSleep, - inhibitWhenAudio: general.idle.inhibitWhenAudio, - timeouts: general.idle.timeouts - }, battery: { warnLevels: general.battery.warnLevels, criticalLevel: general.battery.criticalLevel @@ -179,6 +124,7 @@ Singleton { showMicrophone: bar.status.showMicrophone, showKbLayout: bar.status.showKbLayout, showNetwork: bar.status.showNetwork, + showWifi: bar.status.showWifi, showBluetooth: bar.status.showBluetooth, showBattery: bar.status.showBattery, showLockStatus: bar.status.showLockStatus @@ -248,9 +194,7 @@ Singleton { dragThreshold: launcher.dragThreshold, vimKeybinds: launcher.vimKeybinds, hiddenApps: launcher.hiddenApps, - useFuzzy: { - apps: launcher.useFuzzy.apps, - }, + useFuzzy: launcher.useFuzzy, sizes: { itemWidth: launcher.sizes.itemWidth, itemHeight: launcher.sizes.itemHeight, @@ -315,19 +259,6 @@ Singleton { }; } - function serializeLock(): var { - return { - recolourLogo: lock.recolourLogo, - enableFprint: lock.enableFprint, - maxFprintTries: lock.maxFprintTries, - sizes: { - heightMult: lock.sizes.heightMult, - ratio: lock.sizes.ratio, - centerWidth: lock.sizes.centerWidth - } - }; - } - function serializeUtilities(): var { return { enabled: utilities.enabled, @@ -374,7 +305,6 @@ Singleton { visualiserBars: services.visualiserBars, audioIncrement: services.audioIncrement, maxVolume: services.maxVolume, - smartScheme: services.smartScheme, defaultPlayer: services.defaultPlayer, playerAliases: services.playerAliases }; @@ -383,7 +313,6 @@ Singleton { function serializePaths(): var { return { wallpaper: paths.wallpaper, - face: paths.face, sessionGif: paths.sessionGif, mediaGif: paths.mediaGif }; @@ -407,13 +336,6 @@ Singleton { onLoaded: { try { JSON.parse(text()); - const elapsed = timer.elapsedMs(); - // Only show toast for external changes (not our own saves) and when elapsed time is meaningful - if (adapter.utilities.toasts.configLoaded && !recentlySaved && elapsed > 0) { - Toaster.toast(qsTr("Config loaded"), qsTr("Config loaded in %1ms").arg(elapsed), "rule_settings"); - } else if (adapter.utilities.toasts.configLoaded && recentlySaved && elapsed > 0) { - Toaster.toast(qsTr("Config saved"), qsTr("Config reloaded in %1ms").arg(elapsed), "rule_settings"); - } } catch (e) { Toaster.toast(qsTr("Failed to load config"), e.message, "settings_alert", Toast.Error); } @@ -422,7 +344,6 @@ Singleton { if (err !== FileViewError.FileNotFound) Toaster.toast(qsTr("Failed to read config file"), FileViewError.toString(err), "settings_alert", Toast.Warning); } - onSaveFailed: err => Toaster.toast(qsTr("Failed to save config"), FileViewError.toString(err), "settings_alert", Toast.Error) JsonAdapter { id: adapter @@ -439,7 +360,6 @@ Singleton { property OsdConfig osd: OsdConfig {} property SessionConfig session: SessionConfig {} property WInfoConfig winfo: WInfoConfig {} - property LockConfig lock: LockConfig {} property UtilitiesConfig utilities: UtilitiesConfig {} property SidebarConfig sidebar: SidebarConfig {} property ServiceConfig services: ServiceConfig {} diff --git a/config/GeneralConfig.qml b/config/GeneralConfig.qml index eecca01..2cf99e3 100644 --- a/config/GeneralConfig.qml +++ b/config/GeneralConfig.qml @@ -1,8 +1,8 @@ import Quickshell.Io JsonObject { + property string logo: "" property Apps apps: Apps {} - property Idle idle: Idle {} property Battery battery: Battery {} component Apps: JsonObject { @@ -12,26 +12,6 @@ JsonObject { property list<string> explorer: ["thunar"] } - component Idle: JsonObject { - property bool lockBeforeSleep: true - property bool inhibitWhenAudio: true - property list<var> timeouts: [ - { - timeout: 180, - idleAction: "lock" - }, - { - timeout: 300, - idleAction: "dpms off", - returnAction: "dpms on" - }, - { - timeout: 600, - idleAction: ["systemctl", "suspend-then-hibernate"] - } - ] - } - component Battery: JsonObject { property list<var> warnLevels: [ { diff --git a/config/LauncherConfig.qml b/config/LauncherConfig.qml index 6ab44d1..72f14cf 100644 --- a/config/LauncherConfig.qml +++ b/config/LauncherConfig.qml @@ -8,13 +8,9 @@ JsonObject { property int dragThreshold: 50 property bool vimKeybinds: false property list<string> hiddenApps: [] - property UseFuzzy useFuzzy: UseFuzzy {} + property bool useFuzzy: false property Sizes sizes: Sizes {} - component UseFuzzy: JsonObject { - property bool apps: false - } - component Sizes: JsonObject { property int itemWidth: 600 property int itemHeight: 57 diff --git a/config/LockConfig.qml b/config/LockConfig.qml deleted file mode 100644 index 2af4e2c..0000000 --- a/config/LockConfig.qml +++ /dev/null @@ -1,14 +0,0 @@ -import Quickshell.Io - -JsonObject { - property bool recolourLogo: false - property bool enableFprint: true - property int maxFprintTries: 3 - property Sizes sizes: Sizes {} - - component Sizes: JsonObject { - property real heightMult: 0.7 - property real ratio: 16 / 9 - property int centerWidth: 600 - } -} diff --git a/config/ServiceConfig.qml b/config/ServiceConfig.qml index d083b7a..0bd9145 100644 --- a/config/ServiceConfig.qml +++ b/config/ServiceConfig.qml @@ -10,7 +10,6 @@ JsonObject { property real audioIncrement: 0.1 property real brightnessIncrement: 0.1 property real maxVolume: 1.0 - property bool smartScheme: true property string defaultPlayer: "Spotify" property list<var> playerAliases: [ { diff --git a/config/UserPaths.qml b/config/UserPaths.qml index 0daa4e1..1ff6697 100644 --- a/config/UserPaths.qml +++ b/config/UserPaths.qml @@ -3,7 +3,6 @@ import Quickshell.Io JsonObject { property string wallpaper: "" - property string face: "" property string sessionGif: "root:/assets/len.gif" property string mediaGif: "root:/assets/bongocat.gif" } diff --git a/config/UtilitiesConfig.qml b/config/UtilitiesConfig.qml index 64eca91..0840886 100644 --- a/config/UtilitiesConfig.qml +++ b/config/UtilitiesConfig.qml @@ -22,6 +22,7 @@ JsonObject { property bool capsLockChanged: true property bool numLockChanged: true property bool kbLayoutChanged: true + property bool kbLimit: true property bool vpnChanged: true property bool nowPlaying: false } @@ -23,11 +23,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1767379071, - "narHash": "sha256-EgE0pxsrW9jp9YFMkHL9JMXxcqi/OoumPJYwf+Okucw=", + "lastModified": 1768886240, + "narHash": "sha256-C2TjvwYZ2VDxYWeqvvJ5XPPp6U7H66zeJlRaErJKoEM=", "owner": "nixos", "repo": "nixpkgs", - "rev": "fb7944c166a3b630f177938e478f0378e64ce108", + "rev": "80e4adbcf8992d3fd27ad4964fbb84907f9478b0", "type": "github" }, "original": { @@ -44,11 +44,11 @@ ] }, "locked": { - "lastModified": 1768204289, - "narHash": "sha256-aXPr9W8AEmPz9+dH8CHIwrppKIjtS3mHyOXR2ppH/aw=", + "lastModified": 1768985439, + "narHash": "sha256-qkU4r+l+UPz4dutMMRZSin64HuVZkEv9iFpu9yMWVY0=", "ref": "refs/heads/master", - "rev": "db37dc580afc9db1bc598436649c650138b6166d", - "revCount": 723, + "rev": "191085a8821b35680bba16ce5411fc9dbe912237", + "revCount": 731, "type": "git", "url": "https://git.outfoxxed.me/outfoxxed/quickshell" }, diff --git a/modules/IdleMonitors.qml b/modules/IdleMonitors.qml deleted file mode 100644 index b7ce058..0000000 --- a/modules/IdleMonitors.qml +++ /dev/null @@ -1,51 +0,0 @@ -pragma ComponentBehavior: Bound - -import "lock" -import qs.config -import qs.services -import Caelestia.Internal -import Quickshell -import Quickshell.Wayland - -Scope { - id: root - - required property Lock lock - readonly property bool enabled: !Config.general.idle.inhibitWhenAudio || !Players.list.some(p => p.isPlaying) - - function handleIdleAction(action: var): void { - if (!action) - return; - - if (action === "lock") - lock.lock.locked = true; - else if (action === "unlock") - lock.lock.locked = false; - else if (typeof action === "string") - Hypr.dispatch(action); - else - Quickshell.execDetached(action); - } - - LogindManager { - onAboutToSleep: { - if (Config.general.idle.lockBeforeSleep) - root.lock.lock.locked = true; - } - onLockRequested: root.lock.lock.locked = true - onUnlockRequested: root.lock.lock.unlock() - } - - Variants { - model: Config.general.idle.timeouts - - IdleMonitor { - required property var modelData - - enabled: root.enabled && (modelData.enabled ?? true) - timeout: modelData.timeout - respectInhibitors: modelData.respectInhibitors ?? true - onIsIdleChanged: root.handleIdleAction(isIdle ? modelData.idleAction : modelData.returnAction) - } - } -} diff --git a/modules/background/Background.qml b/modules/background/Background.qml index f7c0a3f..0acce6b 100644 --- a/modules/background/Background.qml +++ b/modules/background/Background.qml @@ -10,7 +10,6 @@ import Quickshell.Wayland import QtQuick Loader { - asynchronous: true active: Config.background.enabled sourceComponent: Variants { @@ -27,16 +26,19 @@ Loader { WlrLayershell.layer: WlrLayer.Background color: "black" - anchors.top: true - anchors.bottom: true - anchors.left: true - anchors.right: true + WlrLayershell.anchors { + top: true + bottom: true + left: true + right: true + } Image { id: wallpaper anchors.fill: parent asynchronous: true + fillMode: Image.PreserveAspectCrop source: Paths.wallpaper ?? "" } diff --git a/modules/bar/components/Clock.qml b/modules/bar/components/Clock.qml index be29c0b..801e93d 100644 --- a/modules/bar/components/Clock.qml +++ b/modules/bar/components/Clock.qml @@ -17,7 +17,6 @@ Column { active: Config.bar.clock.showIcon visible: active - asynchronous: true sourceComponent: MaterialIcon { text: "calendar_month" diff --git a/modules/bar/components/StatusIcons.qml b/modules/bar/components/StatusIcons.qml index 2f52596..442bd2c 100644 --- a/modules/bar/components/StatusIcons.qml +++ b/modules/bar/components/StatusIcons.qml @@ -143,7 +143,7 @@ StyledRect { // Network icon WrappedLoader { name: "network" - active: Config.bar.status.showNetwork + active: Config.bar.status.showNetwork && (! Nmcli.activeEthernet || Config.bar.status.showWifi) sourceComponent: MaterialIcon { animate: true @@ -265,7 +265,6 @@ StyledRect { required property string name Layout.alignment: Qt.AlignHCenter - asynchronous: true visible: active } } diff --git a/modules/bar/components/workspaces/OccupiedBg.qml b/modules/bar/components/workspaces/OccupiedBg.qml index 0364575..da6fa55 100644 --- a/modules/bar/components/workspaces/OccupiedBg.qml +++ b/modules/bar/components/workspaces/OccupiedBg.qml @@ -16,12 +16,15 @@ Item { property list<var> pills: [] onOccupiedChanged: { + if (!occupied) return; let count = 0; const start = groupOffset; const end = start + Config.bar.workspaces.shown; for (const [ws, occ] of Object.entries(occupied)) { if (ws > start && ws <= end && occ) { - if (!occupied[ws - 1]) { + const isFirstInGroup = Number(ws) === start + 1; + const isLastInGroup = Number(ws) === end; + if (isFirstInGroup || !occupied[ws - 1]) { if (pills[count]) pills[count].start = ws; else @@ -30,7 +33,7 @@ Item { })); count++; } - if (!occupied[ws + 1]) + if ((isLastInGroup || !occupied[ws + 1]) && pills[count - 1]) pills[count - 1].end = ws; } } @@ -48,8 +51,8 @@ Item { required property var modelData - readonly property Workspace start: root.workspaces.itemAt(getWsIdx(modelData.start)) ?? null - readonly property Workspace end: root.workspaces.itemAt(getWsIdx(modelData.end)) ?? null + readonly property Workspace start: root.workspaces.count > 0 ? root.workspaces.itemAt(getWsIdx(modelData.start)) ?? null : null + readonly property Workspace end: root.workspaces.count > 0 ? root.workspaces.itemAt(getWsIdx(modelData.end)) ?? null : null function getWsIdx(ws: int): int { let i = ws - 1; diff --git a/modules/bar/popouts/Battery.qml b/modules/bar/popouts/Battery.qml index 35d32c5..ac975e1 100644 --- a/modules/bar/popouts/Battery.qml +++ b/modules/bar/popouts/Battery.qml @@ -40,7 +40,6 @@ Column { anchors.horizontalCenter: parent.horizontalCenter active: PowerProfiles.degradationReason !== PerformanceDegradationReason.None - asynchronous: true height: active ? (item?.implicitHeight ?? 0) : 0 diff --git a/modules/bar/popouts/Bluetooth.qml b/modules/bar/popouts/Bluetooth.qml index 91ac560..676da82 100644 --- a/modules/bar/popouts/Bluetooth.qml +++ b/modules/bar/popouts/Bluetooth.qml @@ -142,7 +142,6 @@ ColumnLayout { } Loader { - asynchronous: true active: device.modelData.bonded sourceComponent: Item { implicitWidth: connectBtn.implicitWidth diff --git a/modules/bar/popouts/Content.qml b/modules/bar/popouts/Content.qml index da993fa..c9a7c5d 100644 --- a/modules/bar/popouts/Content.qml +++ b/modules/bar/popouts/Content.qml @@ -6,6 +6,8 @@ import Quickshell import Quickshell.Services.SystemTray import QtQuick +import "./kblayout" + Item { id: root @@ -114,9 +116,12 @@ Item { Popout { name: "kblayout" - sourceComponent: KbLayout {} + sourceComponent: KbLayout { + wrapper: root.wrapper + } } + Popout { name: "lockstatus" sourceComponent: LockStatus {} diff --git a/modules/bar/popouts/KbLayout.qml b/modules/bar/popouts/KbLayout.qml deleted file mode 100644 index ace5af2..0000000 --- a/modules/bar/popouts/KbLayout.qml +++ /dev/null @@ -1,28 +0,0 @@ -import qs.components -import qs.components.controls -import qs.services -import qs.config -import Quickshell -import QtQuick.Layouts - -ColumnLayout { - id: root - - spacing: Appearance.spacing.normal - - StyledText { - Layout.topMargin: Appearance.padding.normal - Layout.rightMargin: Appearance.padding.normal - text: qsTr("Keyboard layout: %1").arg(Hypr.kbLayoutFull) - font.weight: 500 - } - - TextButton { - Layout.bottomMargin: Appearance.padding.normal - Layout.rightMargin: Appearance.padding.normal - Layout.fillWidth: true - - text: qsTr("Switch layout") - onClicked: Hypr.extras.message("switchxkblayout all next") - } -} diff --git a/modules/bar/popouts/Network.qml b/modules/bar/popouts/Network.qml index 0e99613..5b32e4a 100644 --- a/modules/bar/popouts/Network.qml +++ b/modules/bar/popouts/Network.qml @@ -131,17 +131,13 @@ ColumnLayout { Nmcli.disconnectFromNetwork(); } else { root.connectingToSsid = networkItem.modelData.ssid; - NetworkConnection.handleConnect( - networkItem.modelData, - null, - (network) => { - // Password is required - show password dialog - root.passwordNetwork = network; - root.showPasswordDialog = true; - root.wrapper.currentName = "wirelesspassword"; - } - ); - + NetworkConnection.handleConnect(networkItem.modelData, null, network => { + // Password is required - show password dialog + root.passwordNetwork = network; + root.showPasswordDialog = true; + root.wrapper.currentName = "wirelesspassword"; + }); + // Clear connecting state if connection succeeds immediately (saved profile) // This is handled by the onActiveChanged connection below } @@ -216,7 +212,7 @@ ColumnLayout { anchors.centerIn: parent strokeWidth: Appearance.padding.small / 2 bgColour: "transparent" - implicitHeight: parent.implicitHeight - Appearance.padding.smaller * 2 + implicitSize: parent.implicitHeight - Appearance.padding.smaller * 2 running: Nmcli.scanning } } diff --git a/modules/bar/popouts/TrayMenu.qml b/modules/bar/popouts/TrayMenu.qml index c87c0e1..9b743db 100644 --- a/modules/bar/popouts/TrayMenu.qml +++ b/modules/bar/popouts/TrayMenu.qml @@ -85,7 +85,6 @@ StackView { anchors.right: parent.right active: !item.modelData.isSeparator - asynchronous: true sourceComponent: Item { implicitHeight: label.implicitHeight @@ -118,7 +117,6 @@ StackView { anchors.left: parent.left active: item.modelData.icon !== "" - asynchronous: true sourceComponent: IconImage { implicitSize: label.implicitHeight @@ -155,7 +153,6 @@ StackView { anchors.right: parent.right active: item.modelData.hasChildren - asynchronous: true sourceComponent: MaterialIcon { text: "chevron_right" @@ -169,7 +166,6 @@ StackView { Loader { active: menu.isSubMenu - asynchronous: true sourceComponent: Item { implicitWidth: back.implicitWidth diff --git a/modules/bar/popouts/Wrapper.qml b/modules/bar/popouts/Wrapper.qml index 5ef4f9d..fc74222 100644 --- a/modules/bar/popouts/Wrapper.qml +++ b/modules/bar/popouts/Wrapper.qml @@ -35,8 +35,8 @@ Item { if (mode === "winfo") { detachedMode = mode; } else { - detachedMode = "any"; queuedMode = mode; + detachedMode = "any"; } focus = true; } @@ -101,7 +101,6 @@ Item { id: content shouldBeActive: root.hasCurrent && !root.detachedMode - asynchronous: true anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter @@ -112,7 +111,6 @@ Item { Comp { shouldBeActive: root.detachedMode === "winfo" - asynchronous: true anchors.centerIn: parent sourceComponent: WindowInfo { @@ -123,7 +121,6 @@ Item { Comp { shouldBeActive: root.detachedMode === "any" - asynchronous: true anchors.centerIn: parent sourceComponent: ControlCenter { @@ -173,7 +170,6 @@ Item { property bool shouldBeActive - asynchronous: true active: false opacity: 0 diff --git a/modules/bar/popouts/kblayout/KbLayout.qml b/modules/bar/popouts/kblayout/KbLayout.qml new file mode 100644 index 0000000..f612f58 --- /dev/null +++ b/modules/bar/popouts/kblayout/KbLayout.qml @@ -0,0 +1,158 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.components +import qs.components.controls +import qs.services +import qs.config +import qs.utils + +import "." + +ColumnLayout { + id: root + + required property Item wrapper + + spacing: Appearance.spacing.small + width: Config.bar.sizes.kbLayoutWidth + + KbLayoutModel { id: kb } + + function refresh() { kb.refresh() } + Component.onCompleted: kb.start() + + StyledText { + Layout.topMargin: Appearance.padding.normal + Layout.rightMargin: Appearance.padding.small + text: qsTr("Keyboard Layouts") + font.weight: 500 + } + + ListView { + id: list + model: kb.visibleModel + + Layout.fillWidth: true + Layout.rightMargin: Appearance.padding.small + Layout.topMargin: Appearance.spacing.small + + clip: true + interactive: true + implicitHeight: Math.min(contentHeight, 320) + visible: kb.visibleModel.count > 0 + spacing: Appearance.spacing.small + + add: Transition { + NumberAnimation { properties: "opacity"; from: 0; to: 1; duration: 140 } + NumberAnimation { properties: "y"; duration: 180; easing.type: Easing.OutCubic } + } + remove: Transition { NumberAnimation { properties: "opacity"; to: 0; duration: 100 } } + move: Transition { NumberAnimation { properties: "y"; duration: 180; easing.type: Easing.OutCubic } } + displaced: Transition { NumberAnimation { properties: "y"; duration: 180; easing.type: Easing.OutCubic } } + + delegate: Item { + required property int layoutIndex + required property string label + + width: list.width + height: Math.max(36, rowText.implicitHeight + Appearance.padding.small * 2) + + readonly property bool isDisabled: layoutIndex > 3 + + StateLayer { + id: layer + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + implicitHeight: parent.height - 4 + + radius: Appearance.rounding.full + enabled: !isDisabled + + function onClicked(): void { + if (!isDisabled) + kb.switchTo(layoutIndex); + } + } + + StyledText { + id: rowText + anchors.verticalCenter: layer.verticalCenter + anchors.left: layer.left + anchors.right: layer.right + anchors.leftMargin: Appearance.padding.small + anchors.rightMargin: Appearance.padding.small + text: label + elide: Text.ElideRight + opacity: isDisabled ? 0.4 : 1.0 + } + + ToolTip.visible: isDisabled && layer.containsMouse + ToolTip.text: "XKB limitation: maximum 4 layouts allowed" + } + } + + Rectangle { + visible: kb.activeLabel.length > 0 + Layout.fillWidth: true + Layout.rightMargin: Appearance.padding.small + Layout.topMargin: Appearance.spacing.small + + height: 1 + color: Colours.palette.m3onSurfaceVariant + opacity: 0.35 + } + + RowLayout { + id: activeRow + + visible: kb.activeLabel.length > 0 + Layout.fillWidth: true + Layout.rightMargin: Appearance.padding.small + Layout.topMargin: Appearance.spacing.small + spacing: Appearance.spacing.small + + opacity: 1 + scale: 1 + + MaterialIcon { + text: "keyboard" + color: Colours.palette.m3primary + } + + StyledText { + Layout.fillWidth: true + text: kb.activeLabel + elide: Text.ElideRight + font.weight: 500 + color: Colours.palette.m3primary + } + + Connections { + target: kb + function onActiveLabelChanged() { + if (!activeRow.visible) + return; + popIn.restart(); + } + } + + SequentialAnimation { + id: popIn + running: false + + ParallelAnimation { + NumberAnimation { target: activeRow; property: "opacity"; to: 0.0; duration: 70 } + NumberAnimation { target: activeRow; property: "scale"; to: 0.92; duration: 70 } + } + + ParallelAnimation { + NumberAnimation { target: activeRow; property: "opacity"; to: 1.0; duration: 160; easing.type: Easing.OutCubic } + NumberAnimation { target: activeRow; property: "scale"; to: 1.0; duration: 220; easing.type: Easing.OutBack } + } + } + } +} diff --git a/modules/bar/popouts/kblayout/KbLayoutModel.qml b/modules/bar/popouts/kblayout/KbLayoutModel.qml new file mode 100644 index 0000000..41e45b3 --- /dev/null +++ b/modules/bar/popouts/kblayout/KbLayoutModel.qml @@ -0,0 +1,200 @@ +pragma ComponentBehavior: Bound + +import QtQuick + +import Quickshell +import Quickshell.Io + +import qs.config +import Caelestia + +Item { + id: model + visible: false + + ListModel { id: _visibleModel } + property alias visibleModel: _visibleModel + + property string activeLabel: "" + property int activeIndex: -1 + + function start() { + _xkbXmlBase.running = true; + _getKbLayoutOpt.running = true; + } + + function refresh() { + _notifiedLimit = false; + _getKbLayoutOpt.running = true; + } + + function switchTo(idx) { + _switchProc.command = ["hyprctl", "switchxkblayout", "all", String(idx)]; + _switchProc.running = true; + } + + ListModel { id: _layoutsModel } + + property var _xkbMap: ({}) + property bool _notifiedLimit: false + + Process { + id: _xkbXmlBase + command: ["xmllint", "--xpath", "//layout/configItem[name and description]", "/usr/share/X11/xkb/rules/base.xml"] + stdout: StdioCollector { onStreamFinished: _buildXmlMap(text) } + onRunningChanged: if (!running && (typeof exitCode !== "undefined") && exitCode !== 0) _xkbXmlEvdev.running = true + } + + Process { + id: _xkbXmlEvdev + command: ["xmllint", "--xpath", "//layout/configItem[name and description]", "/usr/share/X11/xkb/rules/evdev.xml"] + stdout: StdioCollector { onStreamFinished: _buildXmlMap(text) } + } + + function _buildXmlMap(xml) { + const map = {}; + + const re = /<name>\s*([^<]+?)\s*<\/name>[\s\S]*?<description>\s*([^<]+?)\s*<\/description>/g; + + let m; + while ((m = re.exec(xml)) !== null) { + const code = (m[1] || "").trim(); + const desc = (m[2] || "").trim(); + if (!code || !desc) continue; + map[code] = _short(desc); + } + + if (Object.keys(map).length === 0) + return; + + _xkbMap = map; + + if (_layoutsModel.count > 0) { + const tmp = []; + for (let i = 0; i < _layoutsModel.count; i++) { + const it = _layoutsModel.get(i); + tmp.push({ layoutIndex: it.layoutIndex, token: it.token, label: _pretty(it.token) }); + } + _layoutsModel.clear(); + tmp.forEach(t => _layoutsModel.append(t)); + _fetchActiveLayouts.running = true; + } + } + + function _short(desc) { + const m = desc.match(/^(.*)\((.*)\)$/); + if (!m) return desc; + const lang = m[1].trim(); + const region = m[2].trim(); + const code = (region.split(/[,\s-]/)[0] || region).slice(0, 2).toUpperCase(); + return `${lang} (${code})`; + } + + Process { + id: _getKbLayoutOpt + command: ["hyprctl", "-j", "getoption", "input:kb_layout"] + stdout: StdioCollector { + onStreamFinished: { + try { + const j = JSON.parse(text); + const raw = (j?.str || j?.value || "").toString().trim(); + if (raw.length) { + _setLayouts(raw); + _fetchActiveLayouts.running = true; + return; + } + } catch (e) {} + _fetchLayoutsFromDevices.running = true; + } + } + } + + Process { + id: _fetchLayoutsFromDevices + command: ["hyprctl", "-j", "devices"] + stdout: StdioCollector { + onStreamFinished: { + try { + const dev = JSON.parse(text); + const kb = dev?.keyboards?.find(k => k.main) || dev?.keyboards?.[0]; + const raw = (kb?.layout || "").trim(); + if (raw.length) _setLayouts(raw); + } catch (e) {} + _fetchActiveLayouts.running = true; + } + } + } + + Process { + id: _fetchActiveLayouts + command: ["hyprctl", "-j", "devices"] + stdout: StdioCollector { + onStreamFinished: { + try { + const dev = JSON.parse(text); + const kb = dev?.keyboards?.find(k => k.main) || dev?.keyboards?.[0]; + const idx = kb?.active_layout_index ?? -1; + + activeIndex = idx >= 0 ? idx : -1; + activeLabel = + (idx >= 0 && idx < _layoutsModel.count) + ? _layoutsModel.get(idx).label + : ""; + } catch (e) { + activeIndex = -1; + activeLabel = ""; + } + + _rebuildVisible(); + } + } + } + + Process { + id: _switchProc + onRunningChanged: if (!running) _fetchActiveLayouts.running = true + } + + function _setLayouts(raw) { + const parts = raw.split(",").map(s => s.trim()).filter(Boolean); + _layoutsModel.clear(); + + const seen = new Set(); + let idx = 0; + + for (const p of parts) { + if (seen.has(p)) continue; + seen.add(p); + _layoutsModel.append({ layoutIndex: idx, token: p, label: _pretty(p) }); + idx++; + } + } + + function _rebuildVisible() { + _visibleModel.clear(); + + let arr = []; + for (let i = 0; i < _layoutsModel.count; i++) + arr.push(_layoutsModel.get(i)); + + arr = arr.filter(i => i.layoutIndex !== activeIndex); + arr.forEach(i => _visibleModel.append(i)); + + if (!Config.utilities.toasts.kbLimit) + return; + + if (_layoutsModel.count > 4) { + Toaster.toast( + qsTr("Keyboard layout limit"), + qsTr("XKB supports only 4 layouts at a time"), + "warning" + ); + } + } + + function _pretty(token) { + const code = token.replace(/\(.*\)$/, "").trim(); + if (_xkbMap[code]) return code.toUpperCase() + " - " + _xkbMap[code]; + return code.toUpperCase() + " - " + code; + } +} diff --git a/modules/controlcenter/ControlCenter.qml b/modules/controlcenter/ControlCenter.qml index fdb824e..043e7e1 100644 --- a/modules/controlcenter/ControlCenter.qml +++ b/modules/controlcenter/ControlCenter.qml @@ -42,7 +42,6 @@ Item { Layout.fillWidth: true Layout.columnSpan: 2 - asynchronous: true active: root.floating visible: active diff --git a/modules/controlcenter/NavRail.qml b/modules/controlcenter/NavRail.qml index ef338b2..e61a741 100644 --- a/modules/controlcenter/NavRail.qml +++ b/modules/controlcenter/NavRail.qml @@ -43,7 +43,6 @@ Item { Loader { Layout.topMargin: Appearance.spacing.large - asynchronous: true active: !root.session.floating visible: active diff --git a/modules/controlcenter/Panes.qml b/modules/controlcenter/Panes.qml index 32ee708..03f74be 100644 --- a/modules/controlcenter/Panes.qml +++ b/modules/controlcenter/Panes.qml @@ -126,11 +126,10 @@ ClippingRectangle { anchors.fill: parent clip: false - asynchronous: true active: false Component.onCompleted: { - pane.updateActive(); + Qt.callLater(pane.updateActive); } onActiveChanged: { diff --git a/modules/controlcenter/Session.qml b/modules/controlcenter/Session.qml index 090ae9b..164e6cd 100644 --- a/modules/controlcenter/Session.qml +++ b/modules/controlcenter/Session.qml @@ -15,6 +15,6 @@ QtObject { readonly property NetworkState network: NetworkState {} readonly property EthernetState ethernet: EthernetState {} - onActiveChanged: activeIndex = panes.indexOf(active) - onActiveIndexChanged: active = panes[activeIndex] + onActiveChanged: activeIndex = Math.max(0, panes.indexOf(active)) + onActiveIndexChanged: if (panes[activeIndex]) active = panes[activeIndex] } diff --git a/modules/controlcenter/audio/AudioPane.qml b/modules/controlcenter/audio/AudioPane.qml index 76122f9..e4a1a64 100644 --- a/modules/controlcenter/audio/AudioPane.qml +++ b/modules/controlcenter/audio/AudioPane.qml @@ -460,6 +460,151 @@ Item { } } } + + SectionHeader { + title: qsTr("Applications") + description: qsTr("Control volume for individual applications") + } + + SectionContainer { + contentSpacing: Appearance.spacing.normal + + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + Repeater { + model: Audio.streams + Layout.fillWidth: true + + delegate: ColumnLayout { + required property var modelData + required property int index + + Layout.fillWidth: true + spacing: Appearance.spacing.smaller + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + MaterialIcon { + text: "apps" + font.pointSize: Appearance.font.size.normal + fill: 0 + } + + StyledText { + Layout.fillWidth: true + elide: Text.ElideRight + maximumLineCount: 1 + text: Audio.getStreamName(modelData) + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + + StyledInputField { + id: streamVolumeInput + Layout.preferredWidth: 70 + validator: IntValidator { bottom: 0; top: 100 } + enabled: !Audio.getStreamMuted(modelData) + + Component.onCompleted: { + text = Math.round(Audio.getStreamVolume(modelData) * 100).toString(); + } + + Connections { + target: modelData + function onAudioChanged() { + if (!streamVolumeInput.hasFocus && modelData?.audio) { + streamVolumeInput.text = Math.round(modelData.audio.volume * 100).toString(); + } + } + } + + onTextEdited: (text) => { + if (hasFocus) { + const val = parseInt(text); + if (!isNaN(val) && val >= 0 && val <= 100) { + Audio.setStreamVolume(modelData, val / 100); + } + } + } + + onEditingFinished: { + const val = parseInt(text); + if (isNaN(val) || val < 0 || val > 100) { + text = Math.round(Audio.getStreamVolume(modelData) * 100).toString(); + } + } + } + + StyledText { + text: "%" + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.normal + opacity: Audio.getStreamMuted(modelData) ? 0.5 : 1 + } + + StyledRect { + implicitWidth: implicitHeight + implicitHeight: streamMuteIcon.implicitHeight + Appearance.padding.normal * 2 + + radius: Appearance.rounding.normal + color: Audio.getStreamMuted(modelData) ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer + + StateLayer { + function onClicked(): void { + Audio.setStreamMuted(modelData, !Audio.getStreamMuted(modelData)); + } + } + + MaterialIcon { + id: streamMuteIcon + + anchors.centerIn: parent + text: Audio.getStreamMuted(modelData) ? "volume_off" : "volume_up" + color: Audio.getStreamMuted(modelData) ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer + } + } + } + + StyledSlider { + Layout.fillWidth: true + implicitHeight: Appearance.padding.normal * 3 + + value: Audio.getStreamVolume(modelData) + enabled: !Audio.getStreamMuted(modelData) + opacity: enabled ? 1 : 0.5 + onMoved: { + Audio.setStreamVolume(modelData, value); + if (!streamVolumeInput.hasFocus) { + streamVolumeInput.text = Math.round(value * 100).toString(); + } + } + + Connections { + target: modelData + function onAudioChanged() { + if (modelData?.audio) { + value = modelData.audio.volume; + } + } + } + } + } + } + + StyledText { + Layout.fillWidth: true + visible: Audio.streams.length === 0 + text: qsTr("No applications currently playing audio") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + horizontalAlignment: Text.AlignHCenter + } + } + } } } } diff --git a/modules/controlcenter/components/SplitPaneLayout.qml b/modules/controlcenter/components/SplitPaneLayout.qml index 8b4f0d9..3609a74 100644 --- a/modules/controlcenter/components/SplitPaneLayout.qml +++ b/modules/controlcenter/components/SplitPaneLayout.qml @@ -49,7 +49,6 @@ RowLayout { anchors.leftMargin: Appearance.padding.large anchors.rightMargin: Appearance.padding.large + Appearance.padding.normal / 2 - asynchronous: true sourceComponent: root.leftContent Component.onCompleted: { @@ -91,7 +90,6 @@ RowLayout { anchors.fill: parent anchors.margins: Appearance.padding.large * 2 - asynchronous: true sourceComponent: root.rightContent Component.onCompleted: { diff --git a/modules/controlcenter/components/SplitPaneWithDetails.qml b/modules/controlcenter/components/SplitPaneWithDetails.qml index e873923..5db5bdb 100644 --- a/modules/controlcenter/components/SplitPaneWithDetails.qml +++ b/modules/controlcenter/components/SplitPaneWithDetails.qml @@ -56,7 +56,6 @@ Item { transformOrigin: Item.Center clip: false - asynchronous: true sourceComponent: rightPaneItem.targetComponent } diff --git a/modules/controlcenter/network/NetworkSettings.qml b/modules/controlcenter/network/NetworkSettings.qml index 22e07cb..81175fb 100644 --- a/modules/controlcenter/network/NetworkSettings.qml +++ b/modules/controlcenter/network/NetworkSettings.qml @@ -4,10 +4,12 @@ import ".." import "../components" import qs.components import qs.components.controls +import qs.components.containers import qs.components.effects import qs.services import qs.config import QtQuick +import QtQuick.Controls import QtQuick.Layouts ColumnLayout { diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml index b430cce..9047174 100644 --- a/modules/controlcenter/network/NetworkingPane.qml +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -153,9 +153,9 @@ Item { rightContent: Component { Item { id: rightPaneItem - - property var ethernetPane: root.session.ethernet.active - property var wirelessPane: root.session.network.active + + property var ethernetPane: root.session && root.session.ethernet ? root.session.ethernet.active : null + property var wirelessPane: root.session && root.session.network ? root.session.network.active : null property var pane: ethernetPane || wirelessPane property string paneId: ethernetPane ? ("eth:" + (ethernetPane.interface || "")) : (wirelessPane ? ("wifi:" + (wirelessPane.ssid || wirelessPane.bssid || "")) : "settings") property Component targetComponent: settingsComponent @@ -173,28 +173,28 @@ Item { } Connections { - target: root.session.ethernet + target: root.session && root.session.ethernet ? root.session.ethernet : null + enabled: target !== null + function onActiveChanged() { - // Clear wireless when ethernet is selected - if (root.session.ethernet.active && root.session.network.active) { - root.session.network.active = null; - return; // Let the network.onActiveChanged handle the update + // Clear others when ethernet is selected + if (root.session && root.session.ethernet && root.session.ethernet.active) { + if (root.session.network && root.session.network.active) root.session.network.active = null; } rightPaneItem.nextComponent = rightPaneItem.getComponentForPane(); - // paneId will automatically update via property binding } } Connections { - target: root.session.network + target: root.session && root.session.network ? root.session.network : null + enabled: target !== null + function onActiveChanged() { - // Clear ethernet when wireless is selected - if (root.session.network.active && root.session.ethernet.active) { - root.session.ethernet.active = null; - return; // Let the ethernet.onActiveChanged handle the update + // Clear others when wireless is selected + if (root.session && root.session.network && root.session.network.active) { + if (root.session.ethernet && root.session.ethernet.active) root.session.ethernet.active = null; } rightPaneItem.nextComponent = rightPaneItem.getComponentForPane(); - // paneId will automatically update via property binding } } diff --git a/modules/dashboard/Content.qml b/modules/dashboard/Content.qml index f5f0e46..79659c5 100644 --- a/modules/dashboard/Content.qml +++ b/modules/dashboard/Content.qml @@ -85,6 +85,7 @@ Item { id: row Pane { + index: 0 sourceComponent: Dash { visibilities: root.visibilities state: root.state @@ -92,16 +93,19 @@ Item { } Pane { + index: 1 sourceComponent: Media { visibilities: root.visibilities } } Pane { + index: 2 sourceComponent: Performance {} } Pane { + index: 3 sourceComponent: Weather {} } } @@ -127,9 +131,15 @@ Item { } component Pane: Loader { + id: pane + + required property int index + Layout.alignment: Qt.AlignTop Component.onCompleted: active = Qt.binding(() => { + // Always keep current tab loaded + if (pane.index === view.currentIndex) return true; const vx = Math.floor(view.visibleArea.xPosition * view.contentWidth); const vex = Math.floor(vx + view.visibleArea.widthRatio * view.contentWidth); return (vx >= x && vx <= x + implicitWidth) || (vex >= x && vex <= x + implicitWidth); diff --git a/modules/dashboard/dash/DateTime.qml b/modules/dashboard/dash/DateTime.qml index bbef067..e740448 100644 --- a/modules/dashboard/dash/DateTime.qml +++ b/modules/dashboard/dash/DateTime.qml @@ -9,8 +9,6 @@ import QtQuick.Layouts Item { id: root - readonly property list<string> timeComponents: Time.format(Config.services.useTwelveHourClock ? "hh:mm:A" : "hh:mm").split(":") - anchors.top: parent.top anchors.bottom: parent.bottom implicitWidth: Config.dashboard.sizes.dateTimeWidth @@ -24,7 +22,7 @@ Item { StyledText { Layout.bottomMargin: -(font.pointSize * 0.4) Layout.alignment: Qt.AlignHCenter - text: root.timeComponents[0] + text: Time.hourStr color: Colours.palette.m3secondary font.pointSize: Appearance.font.size.extraLarge font.family: Appearance.font.family.clock @@ -42,7 +40,7 @@ Item { StyledText { Layout.topMargin: -(font.pointSize * 0.4) Layout.alignment: Qt.AlignHCenter - text: root.timeComponents[1] + text: Time.minuteStr color: Colours.palette.m3secondary font.pointSize: Appearance.font.size.extraLarge font.family: Appearance.font.family.clock @@ -52,12 +50,11 @@ Item { Loader { Layout.alignment: Qt.AlignHCenter - asynchronous: true active: Config.services.useTwelveHourClock visible: active sourceComponent: StyledText { - text: root.timeComponents[2] ?? "" + text: Time.amPmStr color: Colours.palette.m3primary font.pointSize: Appearance.font.size.large font.family: Appearance.font.family.clock diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml index 9fc38bd..00f9596 100644 --- a/modules/drawers/Drawers.qml +++ b/modules/drawers/Drawers.qml @@ -47,7 +47,7 @@ Variants { return 0; const mon = Hypr.monitorFor(screen); - if (mon?.lastIpcObject.specialWorkspace.name || mon?.activeWorkspace?.lastIpcObject.windows > 0) + if (mon?.lastIpcObject?.specialWorkspace?.name || mon?.activeWorkspace?.lastIpcObject?.windows > 0) return 0; const thresholds = []; diff --git a/modules/launcher/services/Apps.qml b/modules/launcher/services/Apps.qml index 79f3754..0c392c6 100644 --- a/modules/launcher/services/Apps.qml +++ b/modules/launcher/services/Apps.qml @@ -48,8 +48,8 @@ Searcher { keys = ["keywords", "name"]; weights = [0.9, 0.1]; } else { - keys = ["name"]; - weights = [1]; + keys = ["name", "execString"]; + weights = [1, 0.1]; if (!search.startsWith(`${prefix}t `)) return query(search).map(e => e.entry); @@ -66,7 +66,7 @@ Searcher { } list: appDb.apps - useFuzzy: Config.launcher.useFuzzy.apps + useFuzzy: Config.launcher.useFuzzy AppDb { id: appDb diff --git a/modules/lock/Center.qml b/modules/lock/Center.qml deleted file mode 100644 index 4e2215b..0000000 --- a/modules/lock/Center.qml +++ /dev/null @@ -1,417 +0,0 @@ -pragma ComponentBehavior: Bound - -import qs.components -import qs.components.controls -import qs.services -import qs.config -import qs.utils -import QtQuick -import QtQuick.Layouts - -ColumnLayout { - id: root - - required property var lock - readonly property list<string> timeComponents: Time.format(Config.services.useTwelveHourClock ? "hh:mm:A" : "hh:mm").split(":") - readonly property real centerScale: Math.min(1, (lock.screen?.height ?? 1440) / 1440) - readonly property int centerWidth: Config.lock.sizes.centerWidth * centerScale - - Layout.preferredWidth: centerWidth - Layout.fillWidth: false - Layout.fillHeight: true - - spacing: Appearance.spacing.large * 2 - - RowLayout { - Layout.alignment: Qt.AlignHCenter - spacing: Appearance.spacing.small - - StyledText { - Layout.alignment: Qt.AlignVCenter - text: root.timeComponents[0] - color: Colours.palette.m3secondary - font.pointSize: Math.floor(Appearance.font.size.extraLarge * 3 * root.centerScale) - font.family: Appearance.font.family.clock - font.bold: true - } - - StyledText { - Layout.alignment: Qt.AlignVCenter - text: ":" - color: Colours.palette.m3primary - font.pointSize: Math.floor(Appearance.font.size.extraLarge * 3 * root.centerScale) - font.family: Appearance.font.family.clock - font.bold: true - } - - StyledText { - Layout.alignment: Qt.AlignVCenter - text: root.timeComponents[1] - color: Colours.palette.m3secondary - font.pointSize: Math.floor(Appearance.font.size.extraLarge * 3 * root.centerScale) - font.family: Appearance.font.family.clock - font.bold: true - } - - Loader { - Layout.leftMargin: Appearance.spacing.small - Layout.alignment: Qt.AlignVCenter - - asynchronous: true - active: Config.services.useTwelveHourClock - visible: active - - sourceComponent: StyledText { - text: root.timeComponents[2] ?? "" - color: Colours.palette.m3primary - font.pointSize: Math.floor(Appearance.font.size.extraLarge * 2 * root.centerScale) - font.family: Appearance.font.family.clock - font.bold: true - } - } - } - - StyledText { - Layout.alignment: Qt.AlignHCenter - Layout.topMargin: -Appearance.padding.large * 2 - - text: Time.format("dddd, d MMMM yyyy") - color: Colours.palette.m3tertiary - font.pointSize: Math.floor(Appearance.font.size.extraLarge * root.centerScale) - font.family: Appearance.font.family.mono - font.bold: true - } - - StyledClippingRect { - Layout.topMargin: Appearance.spacing.large * 2 - Layout.alignment: Qt.AlignHCenter - - implicitWidth: root.centerWidth / 2 - implicitHeight: root.centerWidth / 2 - - color: Colours.tPalette.m3surfaceContainer - radius: Appearance.rounding.full - - MaterialIcon { - anchors.centerIn: parent - - text: "person" - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Math.floor(root.centerWidth / 4) - } - - Image { - id: pfp - - anchors.fill: parent - source: Paths.face - } - } - - StyledRect { - Layout.alignment: Qt.AlignHCenter - - implicitWidth: root.centerWidth * 0.8 - implicitHeight: input.implicitHeight + Appearance.padding.small * 2 - - color: Colours.tPalette.m3surfaceContainer - radius: Appearance.rounding.full - - focus: true - onActiveFocusChanged: { - if (!activeFocus) - forceActiveFocus(); - } - - Keys.onPressed: event => { - if (root.lock.unlocking) - return; - - if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) - inputField.placeholder.animate = false; - - root.lock.pam.handleKey(event); - } - - StateLayer { - hoverEnabled: false - cursorShape: Qt.IBeamCursor - - function onClicked(): void { - parent.forceActiveFocus(); - } - } - - RowLayout { - id: input - - anchors.fill: parent - anchors.margins: Appearance.padding.small - spacing: Appearance.spacing.normal - - Item { - implicitWidth: implicitHeight - implicitHeight: fprintIcon.implicitHeight + Appearance.padding.small * 2 - - MaterialIcon { - id: fprintIcon - - anchors.centerIn: parent - animate: true - text: { - if (root.lock.pam.fprint.tries >= Config.lock.maxFprintTries) - return "fingerprint_off"; - if (root.lock.pam.fprint.active) - return "fingerprint"; - return "lock"; - } - color: root.lock.pam.fprint.tries >= Config.lock.maxFprintTries ? Colours.palette.m3error : Colours.palette.m3onSurface - opacity: root.lock.pam.passwd.active ? 0 : 1 - - Behavior on opacity { - Anim {} - } - } - - CircularIndicator { - anchors.fill: parent - running: root.lock.pam.passwd.active - } - } - - InputField { - id: inputField - - pam: root.lock.pam - } - - StyledRect { - implicitWidth: implicitHeight - implicitHeight: enterIcon.implicitHeight + Appearance.padding.small * 2 - - color: root.lock.pam.buffer ? Colours.palette.m3primary : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) - radius: Appearance.rounding.full - - StateLayer { - color: root.lock.pam.buffer ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface - - function onClicked(): void { - root.lock.pam.passwd.start(); - } - } - - MaterialIcon { - id: enterIcon - - anchors.centerIn: parent - text: "arrow_forward" - color: root.lock.pam.buffer ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface - font.weight: 500 - } - } - } - } - - Item { - Layout.fillWidth: true - Layout.topMargin: -Appearance.spacing.large - - implicitHeight: Math.max(message.implicitHeight, stateMessage.implicitHeight) - - Behavior on implicitHeight { - Anim {} - } - - StyledText { - id: stateMessage - - readonly property string msg: { - if (Hypr.kbLayout !== Hypr.defaultKbLayout) { - if (Hypr.capsLock && Hypr.numLock) - return qsTr("Caps lock and Num lock are ON.\nKeyboard layout: %1").arg(Hypr.kbLayoutFull); - if (Hypr.capsLock) - return qsTr("Caps lock is ON. Kb layout: %1").arg(Hypr.kbLayoutFull); - if (Hypr.numLock) - return qsTr("Num lock is ON. Kb layout: %1").arg(Hypr.kbLayoutFull); - return qsTr("Keyboard layout: %1").arg(Hypr.kbLayoutFull); - } - - if (Hypr.capsLock && Hypr.numLock) - return qsTr("Caps lock and Num lock are ON."); - if (Hypr.capsLock) - return qsTr("Caps lock is ON."); - if (Hypr.numLock) - return qsTr("Num lock is ON."); - - return ""; - } - - property bool shouldBeVisible - - onMsgChanged: { - if (msg) { - if (opacity > 0) { - animate = true; - text = msg; - animate = false; - } else { - text = msg; - } - shouldBeVisible = true; - } else { - shouldBeVisible = false; - } - } - - anchors.left: parent.left - anchors.right: parent.right - - scale: shouldBeVisible && !message.msg ? 1 : 0.7 - opacity: shouldBeVisible && !message.msg ? 1 : 0 - color: Colours.palette.m3onSurfaceVariant - animateProp: "opacity" - - font.family: Appearance.font.family.mono - horizontalAlignment: Qt.AlignHCenter - wrapMode: Text.WrapAtWordBoundaryOrAnywhere - lineHeight: 1.2 - - Behavior on scale { - Anim {} - } - - Behavior on opacity { - Anim {} - } - } - - StyledText { - id: message - - readonly property Pam pam: root.lock.pam - readonly property string msg: { - if (pam.fprintState === "error") - return qsTr("FP ERROR: %1").arg(pam.fprint.message); - if (pam.state === "error") - return qsTr("PW ERROR: %1").arg(pam.passwd.message); - - if (pam.lockMessage) - return pam.lockMessage; - - if (pam.state === "max" && pam.fprintState === "max") - return qsTr("Maximum password and fingerprint attempts reached."); - if (pam.state === "max") { - if (pam.fprint.available) - return qsTr("Maximum password attempts reached. Please use fingerprint."); - return qsTr("Maximum password attempts reached."); - } - if (pam.fprintState === "max") - return qsTr("Maximum fingerprint attempts reached. Please use password."); - - if (pam.state === "fail") { - if (pam.fprint.available) - return qsTr("Incorrect password. Please try again or use fingerprint."); - return qsTr("Incorrect password. Please try again."); - } - if (pam.fprintState === "fail") - return qsTr("Fingerprint not recognized (%1/%2). Please try again or use password.").arg(pam.fprint.tries).arg(Config.lock.maxFprintTries); - - return ""; - } - - anchors.left: parent.left - anchors.right: parent.right - - scale: 0.7 - opacity: 0 - color: Colours.palette.m3error - - font.pointSize: Appearance.font.size.small - font.family: Appearance.font.family.mono - horizontalAlignment: Qt.AlignHCenter - wrapMode: Text.WrapAtWordBoundaryOrAnywhere - - onMsgChanged: { - if (msg) { - if (opacity > 0) { - animate = true; - text = msg; - animate = false; - - exitAnim.stop(); - if (scale < 1) - appearAnim.restart(); - else - flashAnim.restart(); - } else { - text = msg; - exitAnim.stop(); - appearAnim.restart(); - } - } else { - appearAnim.stop(); - flashAnim.stop(); - exitAnim.start(); - } - } - - Connections { - target: root.lock.pam - - function onFlashMsg(): void { - exitAnim.stop(); - if (message.scale < 1) - appearAnim.restart(); - else - flashAnim.restart(); - } - } - - Anim { - id: appearAnim - - target: message - properties: "scale,opacity" - to: 1 - onFinished: flashAnim.restart() - } - - SequentialAnimation { - id: flashAnim - - loops: 2 - - FlashAnim { - to: 0.3 - } - FlashAnim { - to: 1 - } - } - - ParallelAnimation { - id: exitAnim - - Anim { - target: message - property: "scale" - to: 0.7 - duration: Appearance.anim.durations.large - } - Anim { - target: message - property: "opacity" - to: 0 - duration: Appearance.anim.durations.large - } - } - } - } - - component FlashAnim: NumberAnimation { - target: message - property: "opacity" - duration: Appearance.anim.durations.small - easing.type: Easing.Linear - } -} diff --git a/modules/lock/Content.qml b/modules/lock/Content.qml deleted file mode 100644 index daa87ac..0000000 --- a/modules/lock/Content.qml +++ /dev/null @@ -1,27 +0,0 @@ -import qs.components -import qs.services -import qs.config -import QtQuick -import QtQuick.Layouts - -RowLayout { - id: root - - required property var lock - - spacing: Appearance.spacing.large * 2 - - ColumnLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.normal - } - - Center { - lock: root.lock - } - - ColumnLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.normal - } -} diff --git a/modules/lock/InputField.qml b/modules/lock/InputField.qml deleted file mode 100644 index 1acc787..0000000 --- a/modules/lock/InputField.qml +++ /dev/null @@ -1,149 +0,0 @@ -pragma ComponentBehavior: Bound - -import qs.components -import qs.services -import qs.config -import Quickshell -import QtQuick -import QtQuick.Layouts - -Item { - id: root - - required property Pam pam - readonly property alias placeholder: placeholder - property string buffer - - Layout.fillWidth: true - Layout.fillHeight: true - - clip: true - - Connections { - target: root.pam - - function onBufferChanged(): void { - if (root.pam.buffer.length > root.buffer.length) { - charList.bindImWidth(); - } else if (root.pam.buffer.length === 0) { - charList.implicitWidth = charList.implicitWidth; - placeholder.animate = true; - } - - root.buffer = root.pam.buffer; - } - } - - StyledText { - id: placeholder - - anchors.centerIn: parent - - text: { - if (root.pam.passwd.active) - return qsTr("Loading..."); - if (root.pam.state === "max") - return qsTr("You have reached the maximum number of tries"); - return qsTr("Enter your password"); - } - - animate: true - color: root.pam.passwd.active ? Colours.palette.m3secondary : Colours.palette.m3outline - font.pointSize: Appearance.font.size.normal - font.family: Appearance.font.family.mono - - opacity: root.buffer ? 0 : 1 - - Behavior on opacity { - Anim {} - } - } - - ListView { - id: charList - - readonly property int fullWidth: count * (implicitHeight + spacing) - spacing - - function bindImWidth(): void { - imWidthBehavior.enabled = false; - implicitWidth = Qt.binding(() => fullWidth); - imWidthBehavior.enabled = true; - } - - anchors.centerIn: parent - anchors.horizontalCenterOffset: implicitWidth > root.width ? -(implicitWidth - root.width) / 2 : 0 - - implicitWidth: fullWidth - implicitHeight: Appearance.font.size.normal - - orientation: Qt.Horizontal - spacing: Appearance.spacing.small / 2 - interactive: false - - model: ScriptModel { - values: root.buffer.split("") - } - - delegate: StyledRect { - id: ch - - implicitWidth: implicitHeight - implicitHeight: charList.implicitHeight - - color: Colours.palette.m3onSurface - radius: Appearance.rounding.full - - opacity: 0 - scale: 0 - Component.onCompleted: { - opacity = 1; - scale = 1; - } - ListView.onRemove: removeAnim.start() - - SequentialAnimation { - id: removeAnim - - PropertyAction { - target: ch - property: "ListView.delayRemove" - value: true - } - ParallelAnimation { - Anim { - target: ch - property: "opacity" - to: 0 - } - Anim { - target: ch - property: "scale" - to: 0.5 - } - } - PropertyAction { - target: ch - property: "ListView.delayRemove" - value: false - } - } - - Behavior on opacity { - Anim {} - } - - Behavior on scale { - Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial - } - } - } - - Behavior on implicitWidth { - id: imWidthBehavior - - Anim {} - } - } -} diff --git a/modules/lock/Lock.qml b/modules/lock/Lock.qml deleted file mode 100644 index 6fd5277..0000000 --- a/modules/lock/Lock.qml +++ /dev/null @@ -1,55 +0,0 @@ -pragma ComponentBehavior: Bound - -import qs.components.misc -import Quickshell -import Quickshell.Io -import Quickshell.Wayland - -Scope { - property alias lock: lock - - WlSessionLock { - id: lock - - signal unlock - - LockSurface { - lock: lock - pam: pam - } - } - - Pam { - id: pam - - lock: lock - } - - CustomShortcut { - name: "lock" - description: "Lock the current session" - onPressed: lock.locked = true - } - - CustomShortcut { - name: "unlock" - description: "Unlock the current session" - onPressed: lock.unlock() - } - - IpcHandler { - target: "lock" - - function lock(): void { - lock.locked = true; - } - - function unlock(): void { - lock.unlock(); - } - - function isLocked(): bool { - return lock.locked; - } - } -} diff --git a/modules/lock/LockSurface.qml b/modules/lock/LockSurface.qml deleted file mode 100644 index 0c3ce69..0000000 --- a/modules/lock/LockSurface.qml +++ /dev/null @@ -1,231 +0,0 @@ -pragma ComponentBehavior: Bound - -import qs.components -import qs.services -import qs.config -import qs.utils -import Quickshell.Wayland -import QtQuick -import QtQuick.Effects - -WlSessionLockSurface { - id: root - - required property WlSessionLock lock - required property Pam pam - - readonly property alias unlocking: unlockAnim.running - - color: "transparent" - - Connections { - target: root.lock - - function onUnlock(): void { - unlockAnim.start(); - } - } - - SequentialAnimation { - id: unlockAnim - - ParallelAnimation { - Anim { - target: lockContent - properties: "implicitWidth,implicitHeight" - to: lockContent.size - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - Anim { - target: lockBg - property: "radius" - to: lockContent.radius - } - Anim { - target: content - property: "scale" - to: 0 - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - Anim { - target: content - property: "opacity" - to: 0 - duration: Appearance.anim.durations.small - } - Anim { - target: lockIcon - property: "opacity" - to: 1 - duration: Appearance.anim.durations.large - } - Anim { - target: background - property: "opacity" - to: 0 - duration: Appearance.anim.durations.large - } - SequentialAnimation { - PauseAnimation { - duration: Appearance.anim.durations.small - } - Anim { - target: lockContent - property: "opacity" - to: 0 - } - } - } - PropertyAction { - target: root.lock - property: "locked" - value: false - } - } - - ParallelAnimation { - id: initAnim - - running: true - - Anim { - target: background - property: "opacity" - to: 1 - duration: Appearance.anim.durations.large - } - SequentialAnimation { - ParallelAnimation { - Anim { - target: lockContent - property: "scale" - to: 1 - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial - } - Anim { - target: lockContent - property: "rotation" - to: 360 - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.standardAccel - } - } - ParallelAnimation { - Anim { - target: lockIcon - property: "rotation" - to: 360 - easing.bezierCurve: Appearance.anim.curves.standardDecel - } - Anim { - target: lockIcon - property: "opacity" - to: 0 - } - Anim { - target: content - property: "opacity" - to: 1 - } - Anim { - target: content - property: "scale" - to: 1 - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - Anim { - target: lockBg - property: "radius" - to: Appearance.rounding.large * 1.5 - } - Anim { - target: lockContent - property: "implicitWidth" - to: root.screen?.height * Config.lock.sizes.heightMult * Config.lock.sizes.ratio - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - Anim { - target: lockContent - property: "implicitHeight" - to: root.screen?.height * Config.lock.sizes.heightMult - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } - } - } - - Image { - id: background - - anchors.fill: parent - source: Paths.wallpaper ?? "" - opacity: 1 - - layer.enabled: true - layer.effect: MultiEffect { - autoPaddingEnabled: false - blurEnabled: true - blur: 1 - blurMax: 64 - blurMultiplier: 1 - } - } - - Item { - id: lockContent - - readonly property int size: lockIcon.implicitHeight + Appearance.padding.large * 4 - readonly property int radius: size / 4 * Appearance.rounding.scale - - anchors.centerIn: parent - implicitWidth: size - implicitHeight: size - - rotation: 180 - scale: 0 - - StyledRect { - id: lockBg - - anchors.fill: parent - color: Colours.palette.m3surface - radius: parent.radius - opacity: Colours.transparency.enabled ? Colours.transparency.base : 1 - - layer.enabled: true - layer.effect: MultiEffect { - shadowEnabled: true - blurMax: 15 - shadowColor: Qt.alpha(Colours.palette.m3shadow, 0.7) - } - } - - MaterialIcon { - id: lockIcon - - anchors.centerIn: parent - text: "lock" - font.pointSize: Appearance.font.size.extraLarge * 4 - font.bold: true - rotation: 180 - } - - Content { - id: content - - anchors.centerIn: parent - width: (root.screen?.height ?? 0) * Config.lock.sizes.heightMult * Config.lock.sizes.ratio - Appearance.padding.large * 2 - height: (root.screen?.height ?? 0) * Config.lock.sizes.heightMult - Appearance.padding.large * 2 - - lock: root - opacity: 0 - scale: 0 - } - } -} diff --git a/modules/lock/Pam.qml b/modules/lock/Pam.qml deleted file mode 100644 index 0186c2f..0000000 --- a/modules/lock/Pam.qml +++ /dev/null @@ -1,193 +0,0 @@ -import qs.config -import Quickshell -import Quickshell.Io -import Quickshell.Wayland -import Quickshell.Services.Pam -import QtQuick - -Scope { - id: root - - required property WlSessionLock lock - - readonly property alias passwd: passwd - readonly property alias fprint: fprint - property string lockMessage - property string state - property string fprintState - property string buffer - - signal flashMsg - - function handleKey(event: KeyEvent): void { - if (passwd.active || state === "max") - return; - - if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) { - passwd.start(); - } else if (event.key === Qt.Key_Backspace) { - if (event.modifiers & Qt.ControlModifier) { - buffer = ""; - } else { - buffer = buffer.slice(0, -1); - } - } else if (" abcdefghijklmnopqrstuvwxyz1234567890`~!@#$%^&*()-_=+[{]}\\|;:'\",<.>/?".includes(event.text.toLowerCase())) { - // No illegal characters (you are insane if you use unicode in your password) - buffer += event.text; - } - } - - PamContext { - id: passwd - - config: "passwd" - configDirectory: Quickshell.shellDir + "/assets/pam.d" - - onMessageChanged: { - if (message.startsWith("The account is locked")) - root.lockMessage = message; - else if (root.lockMessage && message.endsWith(" left to unlock)")) - root.lockMessage += "\n" + message; - } - - onResponseRequiredChanged: { - if (!responseRequired) - return; - - respond(root.buffer); - root.buffer = ""; - } - - onCompleted: res => { - if (res === PamResult.Success) - return root.lock.unlock(); - - if (res === PamResult.Error) - root.state = "error"; - else if (res === PamResult.MaxTries) - root.state = "max"; - else if (res === PamResult.Failed) - root.state = "fail"; - - root.flashMsg(); - stateReset.restart(); - } - } - - PamContext { - id: fprint - - property bool available - property int tries - property int errorTries - - function checkAvail(): void { - if (!available || !Config.lock.enableFprint || !root.lock.secure) { - abort(); - return; - } - - tries = 0; - errorTries = 0; - start(); - } - - config: "fprint" - configDirectory: Quickshell.shellDir + "/assets/pam.d" - - onCompleted: res => { - if (!available) - return; - - if (res === PamResult.Success) - return root.lock.unlock(); - - if (res === PamResult.Error) { - root.fprintState = "error"; - errorTries++; - if (errorTries < 5) { - abort(); - errorRetry.restart(); - } - } else if (res === PamResult.MaxTries) { - // Isn't actually the real max tries as pam only reports completed - // when max tries is reached. - tries++; - if (tries < Config.lock.maxFprintTries) { - // Restart if not actually real max tries - root.fprintState = "fail"; - start(); - } else { - root.fprintState = "max"; - abort(); - } - } - - root.flashMsg(); - fprintStateReset.start(); - } - } - - Process { - id: availProc - - command: ["sh", "-c", "fprintd-list $USER"] - onExited: code => { - fprint.available = code === 0; - fprint.checkAvail(); - } - } - - Timer { - id: errorRetry - - interval: 800 - onTriggered: fprint.start() - } - - Timer { - id: stateReset - - interval: 4000 - onTriggered: { - if (root.state !== "max") - root.state = ""; - } - } - - Timer { - id: fprintStateReset - - interval: 4000 - onTriggered: { - root.fprintState = ""; - fprint.errorTries = 0; - } - } - - Connections { - target: root.lock - - function onSecureChanged(): void { - if (root.lock.secure) { - availProc.running = true; - root.buffer = ""; - root.state = ""; - root.fprintState = ""; - root.lockMessage = ""; - } - } - - function onUnlock(): void { - fprint.abort(); - } - } - - Connections { - target: Config.lock - - function onEnableFprintChanged(): void { - fprint.checkAvail(); - } - } -} diff --git a/modules/notifications/Notification.qml b/modules/notifications/Notification.qml index b5376cf..8c2d3ec 100644 --- a/modules/notifications/Notification.qml +++ b/modules/notifications/Notification.qml @@ -108,7 +108,6 @@ StyledRect { id: image active: root.hasImage - asynchronous: true anchors.left: parent.left anchors.top: parent.top @@ -135,7 +134,6 @@ StyledRect { id: appIcon active: root.hasAppIcon || !root.hasImage - asynchronous: true anchors.horizontalCenter: root.hasImage ? undefined : image.horizontalCenter anchors.verticalCenter: root.hasImage ? undefined : image.verticalCenter @@ -152,7 +150,6 @@ StyledRect { id: icon active: root.hasAppIcon - asynchronous: true anchors.centerIn: parent @@ -169,7 +166,6 @@ StyledRect { Loader { active: !root.hasAppIcon - asynchronous: true anchors.centerIn: parent anchors.horizontalCenterOffset: -Appearance.font.size.large * 0.02 anchors.verticalCenterOffset: Appearance.font.size.large * 0.02 diff --git a/modules/osd/Content.qml b/modules/osd/Content.qml index 619810f..622f2d7 100644 --- a/modules/osd/Content.qml +++ b/modules/osd/Content.qml @@ -112,7 +112,6 @@ Item { Layout.preferredHeight: shouldBeActive ? Config.osd.sizes.sliderHeight : 0 opacity: shouldBeActive ? 1 : 0 active: opacity > 0 - asynchronous: true visible: active Behavior on Layout.preferredHeight { diff --git a/modules/sidebar/NotifDock.qml b/modules/sidebar/NotifDock.qml index b915ed9..d039d15 100644 --- a/modules/sidebar/NotifDock.qml +++ b/modules/sidebar/NotifDock.qml @@ -87,7 +87,6 @@ Item { Loader { anchors.centerIn: parent - asynchronous: true active: opacity > 0 opacity: root.notifCount > 0 ? 0 : 1 diff --git a/modules/sidebar/NotifGroup.qml b/modules/sidebar/NotifGroup.qml index c230a5c..16aac33 100644 --- a/modules/sidebar/NotifGroup.qml +++ b/modules/sidebar/NotifGroup.qml @@ -109,7 +109,6 @@ StyledRect { Loader { anchors.centerIn: parent - asynchronous: true sourceComponent: root.image ? imageComp : root.appIcon ? appIconComp : materialIconComp } } @@ -117,7 +116,6 @@ StyledRect { Loader { anchors.right: parent.right anchors.bottom: parent.bottom - asynchronous: true active: root.appIcon && root.image sourceComponent: StyledRect { diff --git a/modules/utilities/Content.qml b/modules/utilities/Content.qml index f6d8d63..770a774 100644 --- a/modules/utilities/Content.qml +++ b/modules/utilities/Content.qml @@ -19,8 +19,6 @@ Item { anchors.fill: parent spacing: Appearance.spacing.normal - IdleInhibit {} - Toggles { visibilities: root.visibilities popouts: root.popouts diff --git a/modules/utilities/cards/IdleInhibit.qml b/modules/utilities/cards/IdleInhibit.qml deleted file mode 100644 index 0344e3a..0000000 --- a/modules/utilities/cards/IdleInhibit.qml +++ /dev/null @@ -1,125 +0,0 @@ -import qs.components -import qs.components.controls -import qs.services -import qs.config -import QtQuick -import QtQuick.Layouts - -StyledRect { - id: root - - Layout.fillWidth: true - implicitHeight: layout.implicitHeight + (IdleInhibitor.enabled ? activeChip.implicitHeight + activeChip.anchors.topMargin : 0) + Appearance.padding.large * 2 - - radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - clip: true - - RowLayout { - id: layout - - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledRect { - implicitWidth: implicitHeight - implicitHeight: icon.implicitHeight + Appearance.padding.smaller * 2 - - radius: Appearance.rounding.full - color: IdleInhibitor.enabled ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer - - MaterialIcon { - id: icon - - anchors.centerIn: parent - text: "coffee" - color: IdleInhibitor.enabled ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer - font.pointSize: Appearance.font.size.large - } - } - - ColumnLayout { - Layout.fillWidth: true - spacing: 0 - - StyledText { - Layout.fillWidth: true - text: qsTr("Keep Awake") - font.pointSize: Appearance.font.size.normal - elide: Text.ElideRight - } - - StyledText { - Layout.fillWidth: true - text: IdleInhibitor.enabled ? qsTr("Preventing sleep mode") : qsTr("Normal power management") - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.small - elide: Text.ElideRight - } - } - - StyledSwitch { - checked: IdleInhibitor.enabled - onToggled: IdleInhibitor.enabled = checked - } - } - - Loader { - id: activeChip - - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.topMargin: Appearance.spacing.larger - anchors.bottomMargin: IdleInhibitor.enabled ? Appearance.padding.large : -implicitHeight - anchors.leftMargin: Appearance.padding.large - - opacity: IdleInhibitor.enabled ? 1 : 0 - scale: IdleInhibitor.enabled ? 1 : 0.5 - - Component.onCompleted: active = Qt.binding(() => opacity > 0) - - sourceComponent: StyledRect { - implicitWidth: activeText.implicitWidth + Appearance.padding.normal * 2 - implicitHeight: activeText.implicitHeight + Appearance.padding.small * 2 - - radius: Appearance.rounding.full - color: Colours.palette.m3primary - - StyledText { - id: activeText - - anchors.centerIn: parent - text: qsTr("Active since %1").arg(Qt.formatTime(IdleInhibitor.enabledSince, Config.services.useTwelveHourClock ? "hh:mm a" : "hh:mm")) - color: Colours.palette.m3onPrimary - font.pointSize: Math.round(Appearance.font.size.small * 0.9) - } - } - - Behavior on anchors.bottomMargin { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } - - Behavior on opacity { - Anim { - duration: Appearance.anim.durations.small - } - } - - Behavior on scale { - Anim {} - } - } - - Behavior on implicitHeight { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } -} diff --git a/modules/utilities/cards/Toggles.qml b/modules/utilities/cards/Toggles.qml index e62d3c7..558f2d5 100644 --- a/modules/utilities/cards/Toggles.qml +++ b/modules/utilities/cards/Toggles.qml @@ -82,7 +82,7 @@ StyledRect { icon: "vpn_key" checked: VPN.connected enabled: !VPN.connecting - visible: VPN.enabled + visible: Config.utilities.vpn.provider.some(p => typeof p === "object" ? (p.enabled === true) : false) onClicked: VPN.toggle() } diff --git a/modules/windowinfo/Buttons.qml b/modules/windowinfo/Buttons.qml index d7ef095..89acfe6 100644 --- a/modules/windowinfo/Buttons.qml +++ b/modules/windowinfo/Buttons.qml @@ -118,7 +118,6 @@ ColumnLayout { Loader { active: root.client?.lastIpcObject.floating - asynchronous: true Layout.fillWidth: active Layout.leftMargin: active ? 0 : -parent.spacing Layout.rightMargin: active ? 0 : -parent.spacing diff --git a/modules/windowinfo/Preview.qml b/modules/windowinfo/Preview.qml index d2c5682..4cc0aab 100644 --- a/modules/windowinfo/Preview.qml +++ b/modules/windowinfo/Preview.qml @@ -35,7 +35,6 @@ Item { Loader { anchors.centerIn: parent active: !root.client - asynchronous: true sourceComponent: ColumnLayout { spacing: 0 diff --git a/nix/default.nix b/nix/default.nix index bd246a6..0f2675a 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -124,8 +124,6 @@ in dontStrip = debug; prePatch = '' - substituteInPlace assets/pam.d/fprint \ - --replace-fail pam_fprintd.so /run/current-system/sw/lib/security/pam_fprintd.so substituteInPlace shell.qml \ --replace-fail 'ShellRoot {' 'ShellRoot { settings.watchFiles: false' ''; @@ -140,6 +138,9 @@ in mkdir -p $out/lib ln -s ${extras}/lib/* $out/lib/ + + mkdir -p $out/share/applications + ln -s ${quickshell}/share/applications/* $out/share/applications ''; passthru = { diff --git a/plugin/src/Caelestia/Internal/CMakeLists.txt b/plugin/src/Caelestia/Internal/CMakeLists.txt index 0240de8..d0b7548 100644 --- a/plugin/src/Caelestia/Internal/CMakeLists.txt +++ b/plugin/src/Caelestia/Internal/CMakeLists.txt @@ -4,7 +4,6 @@ qml_module(caelestia-internal circularindicatormanager.hpp circularindicatormanager.cpp hyprdevices.hpp hyprdevices.cpp hyprextras.hpp hyprextras.cpp - logindmanager.hpp logindmanager.cpp LIBRARIES Qt::Gui Qt::Quick diff --git a/plugin/src/Caelestia/Internal/logindmanager.cpp b/plugin/src/Caelestia/Internal/logindmanager.cpp deleted file mode 100644 index 4194ee1..0000000 --- a/plugin/src/Caelestia/Internal/logindmanager.cpp +++ /dev/null @@ -1,65 +0,0 @@ -#include "logindmanager.hpp" - -#include <QtDBus/qdbusconnection.h> -#include <QtDBus/qdbuserror.h> -#include <QtDBus/qdbusinterface.h> -#include <QtDBus/qdbusreply.h> - -namespace caelestia::internal { - -LogindManager::LogindManager(QObject* parent) - : QObject(parent) { - auto bus = QDBusConnection::systemBus(); - if (!bus.isConnected()) { - qWarning() << "LogindManager::LogindManager: failed to connect to system bus:" << bus.lastError().message(); - return; - } - - bool ok = bus.connect("org.freedesktop.login1", "/org/freedesktop/login1", "org.freedesktop.login1.Manager", - "PrepareForSleep", this, SLOT(handlePrepareForSleep(bool))); - - if (!ok) { - qWarning() << "LogindManager::LogindManager: failed to connect to PrepareForSleep signal:" - << bus.lastError().message(); - } - - QDBusInterface login1("org.freedesktop.login1", "/org/freedesktop/login1", "org.freedesktop.login1.Manager", bus); - const QDBusReply<QDBusObjectPath> reply = login1.call("GetSession", "auto"); - if (!reply.isValid()) { - qWarning() << "LogindManager::LogindManager: failed to get session path"; - return; - } - const auto sessionPath = reply.value().path(); - - ok = bus.connect("org.freedesktop.login1", sessionPath, "org.freedesktop.login1.Session", "Lock", this, - SLOT(handleLockRequested())); - - if (!ok) { - qWarning() << "LogindManager::LogindManager: failed to connect to Lock signal:" << bus.lastError().message(); - } - - ok = bus.connect("org.freedesktop.login1", sessionPath, "org.freedesktop.login1.Session", "Unlock", this, - SLOT(handleUnlockRequested())); - - if (!ok) { - qWarning() << "LogindManager::LogindManager: failed to connect to Unlock signal:" << bus.lastError().message(); - } -} - -void LogindManager::handlePrepareForSleep(bool sleep) { - if (sleep) { - emit aboutToSleep(); - } else { - emit resumed(); - } -} - -void LogindManager::handleLockRequested() { - emit lockRequested(); -} - -void LogindManager::handleUnlockRequested() { - emit unlockRequested(); -} - -} // namespace caelestia::internal diff --git a/plugin/src/Caelestia/Internal/logindmanager.hpp b/plugin/src/Caelestia/Internal/logindmanager.hpp deleted file mode 100644 index 72a3401..0000000 --- a/plugin/src/Caelestia/Internal/logindmanager.hpp +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once - -#include <qobject.h> -#include <qqmlintegration.h> - -namespace caelestia::internal { - -class LogindManager : public QObject { - Q_OBJECT - QML_ELEMENT - -public: - explicit LogindManager(QObject* parent = nullptr); - -signals: - void aboutToSleep(); - void resumed(); - void lockRequested(); - void unlockRequested(); - -private slots: - void handlePrepareForSleep(bool sleep); - void handleLockRequested(); - void handleUnlockRequested(); -}; - -} // namespace caelestia::internal diff --git a/plugin/src/Caelestia/cutils.cpp b/plugin/src/Caelestia/cutils.cpp index 0e1778c..6e3bfa9 100644 --- a/plugin/src/Caelestia/cutils.cpp +++ b/plugin/src/Caelestia/cutils.cpp @@ -100,7 +100,7 @@ bool CUtils::copyFile(const QUrl& source, const QUrl& target, bool overwrite) co return false; } - if (overwrite) { + if (overwrite && QFile::exists(target.toLocalFile())) { if (!QFile::remove(target.toLocalFile())) { qWarning() << "CUtils::copyFile: overwrite was specified but failed to remove" << target.toLocalFile(); return false; diff --git a/services/Audio.qml b/services/Audio.qml index 71ccb86..20d9cc8 100644 --- a/services/Audio.qml +++ b/services/Audio.qml @@ -19,15 +19,20 @@ Singleton { acc.sinks.push(node); else if (node.audio) acc.sources.push(node); + } else if (node.isStream && node.audio) { + // Application streams (output streams) + acc.streams.push(node); } return acc; }, { sources: [], - sinks: [] + sinks: [], + streams: [] }) readonly property list<PwNode> sinks: nodes.sinks readonly property list<PwNode> sources: nodes.sources + readonly property list<PwNode> streams: nodes.streams readonly property PwNode sink: Pipewire.defaultAudioSink readonly property PwNode source: Pipewire.defaultAudioSource @@ -79,6 +84,33 @@ Singleton { Pipewire.preferredDefaultAudioSource = newSource; } + function setStreamVolume(stream: PwNode, newVolume: real): void { + if (stream?.ready && stream?.audio) { + stream.audio.muted = false; + stream.audio.volume = Math.max(0, Math.min(Config.services.maxVolume, newVolume)); + } + } + + function setStreamMuted(stream: PwNode, muted: bool): void { + if (stream?.ready && stream?.audio) { + stream.audio.muted = muted; + } + } + + function getStreamVolume(stream: PwNode): real { + return stream?.audio?.volume ?? 0; + } + + function getStreamMuted(stream: PwNode): bool { + return !!stream?.audio?.muted; + } + + function getStreamName(stream: PwNode): string { + if (!stream) return qsTr("Unknown"); + // Try application name first, then description, then name + return stream.applicationName || stream.description || stream.name || qsTr("Unknown Application"); + } + onSinkChanged: { if (!sink?.ready) return; @@ -109,7 +141,7 @@ Singleton { } PwObjectTracker { - objects: [...root.sinks, ...root.sources] + objects: [...root.sinks, ...root.sources, ...root.streams] } CavaProvider { diff --git a/services/IdleInhibitor.qml b/services/IdleInhibitor.qml deleted file mode 100644 index 29409ab..0000000 --- a/services/IdleInhibitor.qml +++ /dev/null @@ -1,56 +0,0 @@ -pragma Singleton - -import Quickshell -import Quickshell.Io -import Quickshell.Wayland - -Singleton { - id: root - - property alias enabled: props.enabled - readonly property alias enabledSince: props.enabledSince - - onEnabledChanged: { - if (enabled) - props.enabledSince = new Date(); - } - - PersistentProperties { - id: props - - property bool enabled - property date enabledSince - - reloadableId: "idleInhibitor" - } - - IdleInhibitor { - enabled: props.enabled - window: PanelWindow { - implicitWidth: 0 - implicitHeight: 0 - color: "transparent" - mask: Region {} - } - } - - IpcHandler { - target: "idleInhibitor" - - function isEnabled(): bool { - return props.enabled; - } - - function toggle(): void { - props.enabled = !props.enabled; - } - - function enable(): void { - props.enabled = true; - } - - function disable(): void { - props.enabled = false; - } - } -} diff --git a/services/Notifs.qml b/services/Notifs.qml index 4a89c7f..2ebc32d 100644 --- a/services/Notifs.qml +++ b/services/Notifs.qml @@ -195,15 +195,8 @@ Singleton { mask: Region {} Image { - anchors.fill: parent - source: Qt.resolvedUrl(notif.image) - fillMode: Image.PreserveAspectCrop - cache: false - asynchronous: true - opacity: 0 - - onStatusChanged: { - if (status !== Image.Ready) + function tryCache(): void { + if (status !== Image.Ready || width != Config.notifs.sizes.image || height != Config.notifs.sizes.image) return; const cacheKey = notif.appName + notif.summary + notif.id; @@ -225,6 +218,17 @@ Singleton { notif.dummyImageLoader.active = false; }); } + + anchors.fill: parent + source: Qt.resolvedUrl(notif.image) + fillMode: Image.PreserveAspectCrop + cache: false + asynchronous: true + opacity: 0 + + onStatusChanged: tryCache() + onWidthChanged: tryCache() + onHeightChanged: tryCache() } } } diff --git a/services/Time.qml b/services/Time.qml index c4b3913..a07d9ef 100644 --- a/services/Time.qml +++ b/services/Time.qml @@ -1,6 +1,8 @@ pragma Singleton +import qs.config import Quickshell +import QtQuick Singleton { property alias enabled: clock.enabled @@ -9,6 +11,12 @@ Singleton { readonly property int minutes: clock.minutes readonly property int seconds: clock.seconds + readonly property string timeStr: format(Config.services.useTwelveHourClock ? "hh:mm:A" : "hh:mm") + readonly property list<string> timeComponents: timeStr.split(":") + readonly property string hourStr: timeComponents[0] ?? "" + readonly property string minuteStr: timeComponents[1] ?? "" + readonly property string amPmStr: timeComponents[2] ?? "" + function format(fmt: string): string { return Qt.formatDateTime(clock.date, fmt); } diff --git a/services/VPN.qml b/services/VPN.qml index 412bda4..431c8ec 100644 --- a/services/VPN.qml +++ b/services/VPN.qml @@ -12,8 +12,11 @@ Singleton { property bool connected: false readonly property bool connecting: connectProc.running || disconnectProc.running - readonly property bool enabled: Config.utilities.vpn.enabled - readonly property var providerInput: (Config.utilities.vpn.provider && Config.utilities.vpn.provider.length > 0) ? Config.utilities.vpn.provider[0] : "wireguard" + readonly property bool enabled: Config.utilities.vpn.provider.some(p => typeof p === "object" ? (p.enabled === true) : false) + readonly property var providerInput: { + const enabledProvider = Config.utilities.vpn.provider.find(p => typeof p === "object" ? (p.enabled === true) : false); + return enabledProvider || "wireguard"; + } readonly property bool isCustomProvider: typeof providerInput === "object" readonly property string providerName: isCustomProvider ? (providerInput.name || "custom") : String(providerInput) readonly property string interfaceName: isCustomProvider ? (providerInput.interface || "") : "" @@ -5,19 +5,12 @@ import "modules" import "modules/drawers" import "modules/background" -import "modules/lock" import Quickshell ShellRoot { Background {} Drawers {} - Lock { - id: lock - } Shortcuts {} BatteryMonitor {} - IdleMonitors { - lock: lock - } } diff --git a/utils/Paths.qml b/utils/Paths.qml index 3b7e84f..8350fb1 100644 --- a/utils/Paths.qml +++ b/utils/Paths.qml @@ -19,7 +19,6 @@ Singleton { readonly property string imagecache: `${cache}/imagecache` readonly property string notifimagecache: `${imagecache}/notifs` readonly property string wallpaper: Quickshell.env("CAELESTIA_WALLPAPER") || absolutePath(Config.paths.wallpaper) - readonly property string face: Quickshell.env("CAELESTIA_FACE") || absolutePath(Config.paths.face) readonly property string libdir: Quickshell.env("CAELESTIA_LIB_DIR") || "/usr/lib/caelestia" function toLocalFile(path: url): string { @@ -28,7 +27,7 @@ Singleton { } function absolutePath(path: string): string { - return toLocalFile(path.replace("~", home)); + return toLocalFile(path.replace(/~|(\$({?)HOME(}?))+/, home)); } function shortenHome(path: string): string { diff --git a/utils/SysInfo.qml b/utils/SysInfo.qml index ab2699d..19aa4a7 100644 --- a/utils/SysInfo.qml +++ b/utils/SysInfo.qml @@ -1,5 +1,7 @@ pragma Singleton +import qs.config +import qs.utils import Quickshell import Quickshell.Io import QtQuick @@ -34,13 +36,27 @@ Singleton { root.osIdLike = fd("ID_LIKE").split(" "); const logo = Quickshell.iconPath(fd("LOGO"), true); - if (logo) { + if (Config.general.logo === "caelestia") { + root.osLogo = Qt.resolvedUrl(`${Quickshell.shellDir}/assets/logo.svg`); + root.isDefaultLogo = true; + } else if (Config.general.logo) { + root.osLogo = Quickshell.iconPath(Config.general.logo, true) || "file://" + Paths.absolutePath(Config.general.logo); + root.isDefaultLogo = false; + } else if (logo) { root.osLogo = logo; root.isDefaultLogo = false; } } } + Connections { + target: Config.general + + function onLogoChanged(): void { + osRelease.reload(); + } + } + Timer { running: true repeat: true |