diff --git a/dots/.config/hypr/hyprland/keybinds.conf b/dots/.config/hypr/hyprland/keybinds.conf index 6032ab65a..0cca263f1 100644 --- a/dots/.config/hypr/hyprland/keybinds.conf +++ b/dots/.config/hypr/hyprland/keybinds.conf @@ -53,7 +53,7 @@ bindd = Ctrl+Super, T, Toggle wallpaper selector, global, quickshell:wallpaperSe bindd = Ctrl+Super+Alt, T, Select random wallpaper, global, quickshell:wallpaperSelectorRandom # Random wallpaper bindd = Ctrl+Super, T, Change wallpaper, exec, qs -c $qsConfig ipc call TEST_ALIVE || ~/.config/quickshell/$qsConfig/scripts/colors/switchwall.sh # [hidden] Change wallpaper (fallback) bind = Ctrl+Super, R, exec, killall ags agsv1 gjs ydotool qs quickshell; qs -c $qsConfig & # Restart widgets -bind = Super+Alt, W, global, quickshell:panelFamilyCycle # Cycle panel family +bind = Ctrl+Super, P, global, quickshell:panelFamilyCycle # Cycle panel family ##! Utilities # Screenshot, Record, OCR, Color picker, Clipboard history diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/.svg b/dots/.config/quickshell/ii/assets/icons/fluent/.svg new file mode 100644 index 000000000..c047f0c03 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/.svg @@ -0,0 +1,35 @@ + + + + + diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/checkmark-filled.svg b/dots/.config/quickshell/ii/assets/icons/fluent/checkmark-filled.svg new file mode 100644 index 000000000..966863066 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/checkmark-filled.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/checkmark.svg b/dots/.config/quickshell/ii/assets/icons/fluent/checkmark.svg new file mode 100644 index 000000000..07380988b --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/checkmark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/empty.svg b/dots/.config/quickshell/ii/assets/icons/fluent/empty.svg new file mode 100644 index 000000000..c047f0c03 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/empty.svg @@ -0,0 +1,35 @@ + + + + + diff --git a/dots/.config/quickshell/ii/modules/common/Config.qml b/dots/.config/quickshell/ii/modules/common/Config.qml index aebb91b66..341df9045 100644 --- a/dots/.config/quickshell/ii/modules/common/Config.qml +++ b/dots/.config/quickshell/ii/modules/common/Config.qml @@ -418,6 +418,8 @@ Singleton { property real scale: 0.18 // Relative to screen size property real rows: 2 property real columns: 5 + property bool orderRightLeft: false + property bool orderBottomUp: false property bool centerIcons: true } diff --git a/dots/.config/quickshell/ii/modules/common/Directories.qml b/dots/.config/quickshell/ii/modules/common/Directories.qml index 9afbed44b..78b0a9edb 100644 --- a/dots/.config/quickshell/ii/modules/common/Directories.qml +++ b/dots/.config/quickshell/ii/modules/common/Directories.qml @@ -44,6 +44,7 @@ Singleton { property string wallpaperSwitchScriptPath: FileUtils.trimFileProtocol(`${Directories.scriptPath}/colors/switchwall.sh`) property string defaultAiPrompts: Quickshell.shellPath("defaults/ai/prompts") property string userAiPrompts: FileUtils.trimFileProtocol(`${Directories.shellConfig}/ai/prompts`) + property string userActions: FileUtils.trimFileProtocol(`${Directories.shellConfig}/actions`) property string aiChats: FileUtils.trimFileProtocol(`${Directories.state}/user/ai/chats`) property string aiTranslationScriptPath: FileUtils.trimFileProtocol(`${Directories.scriptPath}/ai/gemini-translate.sh`) property string recordScriptPath: FileUtils.trimFileProtocol(`${Directories.scriptPath}/videos/record.sh`) @@ -59,6 +60,7 @@ Singleton { Quickshell.execDetached(["bash", "-c", `rm -rf '${latexOutput}'; mkdir -p '${latexOutput}'`]) Quickshell.execDetached(["bash", "-c", `rm -rf '${cliphistDecode}'; mkdir -p '${cliphistDecode}'`]) Quickshell.execDetached(["mkdir", "-p", `${aiChats}`]) + Quickshell.execDetached(["mkdir", "-p", `${userActions}`]) Quickshell.execDetached(["rm", "-rf", `${tempImages}`]) } } diff --git a/dots/.config/quickshell/ii/modules/common/Persistent.qml b/dots/.config/quickshell/ii/modules/common/Persistent.qml index 478cadd10..247884028 100644 --- a/dots/.config/quickshell/ii/modules/common/Persistent.qml +++ b/dots/.config/quickshell/ii/modules/common/Persistent.qml @@ -63,6 +63,10 @@ Singleton { property real temperature: 0.5 } + property JsonObject cheatsheet: JsonObject { + property int tabIndex: 0 + } + property JsonObject sidebar: JsonObject { property JsonObject bottomGroup: JsonObject { property bool collapsed: false diff --git a/dots/.config/quickshell/ii/modules/common/models/IndexModel.qml b/dots/.config/quickshell/ii/modules/common/models/IndexModel.qml new file mode 100644 index 000000000..0cf30cdea --- /dev/null +++ b/dots/.config/quickshell/ii/modules/common/models/IndexModel.qml @@ -0,0 +1,6 @@ +import Quickshell + +ScriptModel { + required property int count + values: Array(count).map((_, i) => i) +} diff --git a/dots/.config/quickshell/ii/modules/common/panels/lock/LockContext.qml b/dots/.config/quickshell/ii/modules/common/panels/lock/LockContext.qml index e6454b476..60d5ac949 100644 --- a/dots/.config/quickshell/ii/modules/common/panels/lock/LockContext.qml +++ b/dots/.config/quickshell/ii/modules/common/panels/lock/LockContext.qml @@ -72,7 +72,7 @@ Scope { } function stopFingerPam() { - if (fingerPam.running) { + if (fingerPam.active) { fingerPam.abort(); } } diff --git a/dots/.config/quickshell/ii/modules/common/panels/lock/LockScreen.qml b/dots/.config/quickshell/ii/modules/common/panels/lock/LockScreen.qml index 9e4b9bd94..5a1b24d7e 100644 --- a/dots/.config/quickshell/ii/modules/common/panels/lock/LockScreen.qml +++ b/dots/.config/quickshell/ii/modules/common/panels/lock/LockScreen.qml @@ -92,7 +92,6 @@ Scope { WlSessionLock { id: lock locked: GlobalStates.screenLocked - surface: root.sessionLockSurface } diff --git a/dots/.config/quickshell/ii/modules/common/widgets/StyledListView.qml b/dots/.config/quickshell/ii/modules/common/widgets/StyledListView.qml index 26518cfb1..4267e7bcd 100644 --- a/dots/.config/quickshell/ii/modules/common/widgets/StyledListView.qml +++ b/dots/.config/quickshell/ii/modules/common/widgets/StyledListView.qml @@ -100,7 +100,7 @@ ListView { to: 1, }), ] : [] - } + } move: Transition { animations: root.animateMovement ? [ diff --git a/dots/.config/quickshell/ii/modules/ii/cheatsheet/Cheatsheet.qml b/dots/.config/quickshell/ii/modules/ii/cheatsheet/Cheatsheet.qml index d133fa654..f7fb68f68 100644 --- a/dots/.config/quickshell/ii/modules/ii/cheatsheet/Cheatsheet.qml +++ b/dots/.config/quickshell/ii/modules/ii/cheatsheet/Cheatsheet.qml @@ -4,6 +4,7 @@ import qs.modules.common.widgets import QtQuick import QtQuick.Controls import QtQuick.Layouts +import Qt.labs.synchronizer import Qt5Compat.GraphicalEffects import Quickshell.Io import Quickshell @@ -135,7 +136,10 @@ Scope { // Scope ToolbarTabBar { id: tabBar tabButtonList: root.tabButtonList - currentIndex: swipeView.currentIndex + + Synchronizer on currentIndex { + property alias source: swipeView.currentIndex + } } } @@ -144,8 +148,11 @@ Scope { // Scope Layout.topMargin: 5 Layout.fillWidth: true Layout.fillHeight: true - currentIndex: tabBar.currentIndex spacing: 10 + currentIndex: Persistent.states.cheatsheet.tabIndex + onCurrentIndexChanged: { + Persistent.states.cheatsheet.tabIndex = currentIndex; + } implicitWidth: Math.max.apply(null, contentChildren.map(child => child.implicitWidth || 0)) implicitHeight: Math.max.apply(null, contentChildren.map(child => child.implicitHeight || 0)) diff --git a/dots/.config/quickshell/ii/modules/ii/overview/OverviewWidget.qml b/dots/.config/quickshell/ii/modules/ii/overview/OverviewWidget.qml index db424e0e6..8b791f44a 100644 --- a/dots/.config/quickshell/ii/modules/ii/overview/OverviewWidget.qml +++ b/dots/.config/quickshell/ii/modules/ii/overview/OverviewWidget.qml @@ -50,6 +50,21 @@ Item { property Component windowComponent: OverviewWindow {} property list windowWidgets: [] + + function getWsRow(ws) { + // 1-indexed workspace, 0-indexed row + var normalRow = Math.floor((ws - 1) / Config.options.overview.columns) % Config.options.overview.rows; + return (Config.options.overview.orderBottomUp ? Config.options.overview.rows - normalRow - 1 : normalRow); + } + function getWsColumn(ws) { + // 1-indexed workspace, 0-indexed column + var normalCol = (ws - 1) % Config.options.overview.columns; + return (Config.options.overview.orderRightLeft ? Config.options.overview.columns - normalCol - 1 : normalCol); + } + function getWsInCell(ri, ci) { + // 1-indexed workspace, 0-indexed row and column index + return (Config.options.overview.orderBottomUp ? Config.options.overview.rows - ri - 1 : ri) * Config.options.overview.columns + (Config.options.overview.orderRightLeft ? Config.options.overview.columns - ci - 1 : ci) + 1 + } StyledRectangularShadow { target: overviewBackground @@ -85,7 +100,7 @@ Item { id: workspace required property int index property int colIndex: index - property int workspaceValue: root.workspaceGroup * root.workspacesShown + row.index * Config.options.overview.columns + colIndex + 1 + property int workspaceValue: root.workspaceGroup * root.workspacesShown + getWsInCell(row.index, colIndex) property color defaultWorkspaceColor: ColorUtils.mix(Appearance.colors.colBackgroundSurfaceContainer, Appearance.colors.colSurfaceContainerHigh, 0.8) property color hoveredWorkspaceColor: ColorUtils.mix(defaultWorkspaceColor, Appearance.colors.colLayer1Hover, 0.1) property color hoveredBorderColor: Appearance.colors.colLayer2Hover @@ -182,8 +197,8 @@ Item { property bool atInitPosition: (initX == x && initY == y) // Offset on the canvas - property int workspaceColIndex: (windowData?.workspace.id - 1) % Config.options.overview.columns - property int workspaceRowIndex: Math.floor((windowData?.workspace.id - 1) % root.workspacesShown / Config.options.overview.columns) + property int workspaceColIndex: getWsColumn(windowData?.workspace.id) + property int workspaceRowIndex: getWsRow(windowData?.workspace.id) xOffset: (root.workspaceImplicitWidth + workspaceSpacing) * workspaceColIndex yOffset: (root.workspaceImplicitHeight + workspaceSpacing) * workspaceRowIndex property real xWithinWorkspaceWidget: Math.max((windowData?.at[0] - (monitor?.x ?? 0) - monitorData?.reserved[0]) * root.scale, 0) @@ -286,9 +301,8 @@ Item { Rectangle { // Focused workspace indicator id: focusedWorkspaceIndicator - property int activeWorkspaceInGroup: monitor.activeWorkspace?.id - (root.workspaceGroup * root.workspacesShown) - property int rowIndex: Math.floor((activeWorkspaceInGroup - 1) / Config.options.overview.columns) - property int colIndex: (activeWorkspaceInGroup - 1) % Config.options.overview.columns + property int rowIndex: getWsRow(monitor.activeWorkspace?.id) + property int colIndex: getWsColumn(monitor.activeWorkspace?.id) x: (root.workspaceImplicitWidth + workspaceSpacing) * colIndex y: (root.workspaceImplicitHeight + workspaceSpacing) * rowIndex z: root.windowZ diff --git a/dots/.config/quickshell/ii/modules/settings/InterfaceConfig.qml b/dots/.config/quickshell/ii/modules/settings/InterfaceConfig.qml index b99e291b0..f94f1e811 100644 --- a/dots/.config/quickshell/ii/modules/settings/InterfaceConfig.qml +++ b/dots/.config/quickshell/ii/modules/settings/InterfaceConfig.qml @@ -739,6 +739,45 @@ ContentPage { } } } + ConfigRow { + uniform: true + ConfigSelectionArray { + currentValue: Config.options.overview.orderRightLeft + onSelected: newValue => { + Config.options.overview.orderRightLeft = newValue + } + options: [ + { + displayName: Translation.tr("Left to right"), + icon: "arrow_forward", + value: 0 + }, + { + displayName: Translation.tr("Right to left"), + icon: "arrow_back", + value: 1 + } + ] + } + ConfigSelectionArray { + currentValue: Config.options.overview.orderBottomUp + onSelected: newValue => { + Config.options.overview.orderBottomUp = newValue + } + options: [ + { + displayName: Translation.tr("Top-down"), + icon: "arrow_downward", + value: 0 + }, + { + displayName: Translation.tr("Bottom-up"), + icon: "arrow_upward", + value: 1 + } + ] + } + } } ContentSection { diff --git a/dots/.config/quickshell/ii/modules/waffle/bar/tasks/Tasks.qml b/dots/.config/quickshell/ii/modules/waffle/bar/tasks/Tasks.qml index f33c6e12a..a2670118e 100644 --- a/dots/.config/quickshell/ii/modules/waffle/bar/tasks/Tasks.qml +++ b/dots/.config/quickshell/ii/modules/waffle/bar/tasks/Tasks.qml @@ -9,8 +9,8 @@ MouseArea { id: root Layout.fillHeight: true - implicitHeight: row.implicitHeight - implicitWidth: row.implicitWidth + implicitHeight: appRow.implicitHeight + implicitWidth: appRow.implicitWidth hoverEnabled: true function showPreviewPopup(appEntry, button) { @@ -21,31 +21,31 @@ MouseArea { animation: Looks.transition.move.createObject(this) } - // Apps row - RowLayout { - id: row + WListView { + id: appRow anchors { top: parent.top bottom: parent.bottom } + orientation: Qt.Horizontal spacing: 0 + implicitWidth: contentWidth + clip: true + interactive: false + // TODO: Include only apps (and windows) in current workspace only | wait, does that even make sense in a Hyprland workflow? + model: ScriptModel { + objectProp: "appId" + values: TaskbarApps.apps.filter(app => app.appId !== "SEPARATOR") + } + delegate: TaskAppButton { + required property var modelData + appEntry: modelData - Repeater { - // TODO: Include only apps (and windows) in current workspace only | wait, does that even make sense in a Hyprland workflow? - model: ScriptModel { - objectProp: "appId" - values: TaskbarApps.apps.filter(app => app.appId !== "SEPARATOR") + onHoverPreviewRequested: { + root.showPreviewPopup(appEntry, this); } - delegate: TaskAppButton { - required property var modelData - appEntry: modelData - - onHoverPreviewRequested: { - root.showPreviewPopup(appEntry, this) - } - onHoverPreviewDismissed: { - previewPopup.close() - } + onHoverPreviewDismissed: { + previewPopup.close(); } } } @@ -56,5 +56,4 @@ MouseArea { tasksHovered: root.containsMouse anchor.window: root.QsWindow.window } - } diff --git a/dots/.config/quickshell/ii/modules/waffle/lock/WaffleLock.qml b/dots/.config/quickshell/ii/modules/waffle/lock/WaffleLock.qml index 581ce9152..b1ff6f353 100644 --- a/dots/.config/quickshell/ii/modules/waffle/lock/WaffleLock.qml +++ b/dots/.config/quickshell/ii/modules/waffle/lock/WaffleLock.qml @@ -36,15 +36,34 @@ LockScreen { Image { id: bg z: 0 - anchors.fill: parent + width: parent.width + height: parent.height + onStatusChanged: { + if (status === Image.Ready) { + print("Lock wallpaper loaded"); + print(lockSurfaceItem.height); + y = -lockSurfaceItem.height; + openAnim.restart(); + } + } sourceSize: Qt.size(lockSurfaceItem.width, lockSurfaceItem.height) source: Config.options.background.wallpaperPath fillMode: Image.PreserveAspectCrop + + PropertyAnimation { + id: openAnim + target: bg + property: "y" + to: 0 + duration: 350 + easing.type: Easing.BezierSpline + easing.bezierCurve: Looks.transition.easing.bezierCurve.easeIn + } } GaussianBlur { z: 1 - anchors.fill: parent + anchors.fill: bg source: bg radius: 100 samples: radius * 2 + 1 @@ -67,7 +86,7 @@ LockScreen { Interactables { id: interactables z: 2 - anchors.fill: parent + anchors.fill: bg } } @@ -83,12 +102,31 @@ LockScreen { // } function switchToFocusedView() { - root.passwordView = true; + switchToPasswordViewAnim.restart(); + } + + SequentialAnimation { + id: switchToPasswordViewAnim + PropertyAnimation { + target: unfocusedContent + property: "y" + from: 0 + to: -height * 1.1 + duration: 250 + easing.type: Easing.BezierSpline + easing.bezierCurve: Looks.transition.easing.bezierCurve.easeIn + } + ScriptAction { + script: { + root.passwordView = true; + } + } } Item { id: unfocusedContent - anchors.fill: parent + width: parent.width + height: parent.height visible: !root.passwordView ClockTextGroup { anchors { diff --git a/dots/.config/quickshell/ii/modules/waffle/looks/Looks.qml b/dots/.config/quickshell/ii/modules/waffle/looks/Looks.qml index 51e7fe40c..8120aa85e 100644 --- a/dots/.config/quickshell/ii/modules/waffle/looks/Looks.qml +++ b/dots/.config/quickshell/ii/modules/waffle/looks/Looks.qml @@ -177,7 +177,7 @@ Singleton { property Component color: Component { ColorAnimation { - duration: 120 + duration: 80 easing.type: Easing.BezierSpline easing.bezierCurve: transition.easing.bezierCurve.easeIn } diff --git a/dots/.config/quickshell/ii/modules/waffle/looks/WListView.qml b/dots/.config/quickshell/ii/modules/waffle/looks/WListView.qml index bc567762a..3e506ef99 100644 --- a/dots/.config/quickshell/ii/modules/waffle/looks/WListView.qml +++ b/dots/.config/quickshell/ii/modules/waffle/looks/WListView.qml @@ -6,5 +6,14 @@ import QtQuick.Controls ListView { id: root + boundsBehavior: Flickable.DragOverBounds + ScrollBar.vertical: WScrollBar {} + + displaced: Transition { + animations: [Looks.transition.enter.createObject(this, { + property: "y" + })] + } + } diff --git a/dots/.config/quickshell/ii/modules/waffle/notificationCenter/NotificationPaneContent.qml b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/NotificationPaneContent.qml index 368829865..5e9582bf1 100644 --- a/dots/.config/quickshell/ii/modules/waffle/notificationCenter/NotificationPaneContent.qml +++ b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/NotificationPaneContent.qml @@ -53,10 +53,9 @@ BodyRectangle { } } - StyledListView { + WListView { Layout.fillWidth: true Layout.fillHeight: true - animateAppearance: false clip: true model: Notifications.appNameList diff --git a/dots/.config/quickshell/ii/modules/waffle/notificationCenter/WNotificationDismissAnim.qml b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/WNotificationDismissAnim.qml new file mode 100644 index 000000000..8d3972c40 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/WNotificationDismissAnim.qml @@ -0,0 +1,32 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions +import qs.modules.waffle.looks + +SequentialAnimation { + id: root + + required property var target + + PropertyAction { + target: root.target + property: "ListView.delayRemove" + value: true + } + NumberAnimation { + target: root.target + property: "x" + to: root.target.width + duration: 250 + easing.type: Easing.BezierSpline + easing.bezierCurve: Looks.transition.easing.bezierCurve.easeIn + } + PropertyAction { + target: root.target + property: "ListView.delayRemove" + value: false + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/notificationCenter/WNotificationGroup.qml b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/WNotificationGroup.qml index 658bc03ae..88c44136e 100644 --- a/dots/.config/quickshell/ii/modules/waffle/notificationCenter/WNotificationGroup.qml +++ b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/WNotificationGroup.qml @@ -1,3 +1,4 @@ +pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts import Quickshell @@ -18,10 +19,44 @@ MouseArea { implicitWidth: contentLayout.implicitWidth implicitHeight: contentLayout.implicitHeight + function dismissAll() { + root.notifications.forEach(notif => { + Qt.callLater(() => { + Notifications.discardNotification(notif.notificationId); + }); + }); + removeAnimation.start(); + } + + WNotificationDismissAnim { + id: removeAnimation + target: root + } + + property real dragDismissThreshold: 100 + drag { + axis: Drag.XAxis + target: contentLayout + minimumX: 0 + onActiveChanged: { + if (drag.active) + return; + if (contentLayout.x > root.dragDismissThreshold) { + root.dismissAll(); + } else { + contentLayout.x = 0; + } + } + } + ColumnLayout { id: contentLayout - anchors.fill: parent spacing: 4 + width: root.width + + Behavior on x { + animation: Looks.transition.enter.createObject(this) + } GroupHeader { id: notifHeader @@ -29,7 +64,9 @@ MouseArea { Layout.margins: 11 } - ListView { + WListView { + Layout.leftMargin: -Math.min(35, contentLayout.x) + Layout.rightMargin: -Layout.leftMargin Layout.fillWidth: true implicitWidth: notifHeader.implicitWidth implicitHeight: contentHeight @@ -40,14 +77,20 @@ MouseArea { objectProp: "notificationId" } delegate: WSingleNotification { + id: singleNotif required property int index required property var modelData + width: ListView.view.width notification: modelData + groupExpandControlMessage: { - if (root.notifications.length <= 1) return ""; - if (!root.expanded) return Translation.tr("+%1 notifications").arg(root.notifications.length - 1); - if (index === root.notifications.length - 1) return Translation.tr("See fewer"); + if (root.notifications.length <= 1) + return ""; + if (!root.expanded) + return Translation.tr("+%1 notifications").arg(root.notifications.length - 1); + if (index === root.notifications.length - 1) + return Translation.tr("See fewer"); return ""; } onGroupExpandToggle: { @@ -94,11 +137,7 @@ MouseArea { Layout.rightMargin: 3 icon.name: "dismiss" onClicked: { - root.notifications.forEach(notif => { - Qt.callLater(() => { - Notifications.discardNotification(notif.notificationId); - }); - }); + root.dismissAll(); } } } diff --git a/dots/.config/quickshell/ii/modules/waffle/notificationCenter/WSingleNotification.qml b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/WSingleNotification.qml index 895ab6892..350e7b0ab 100644 --- a/dots/.config/quickshell/ii/modules/waffle/notificationCenter/WSingleNotification.qml +++ b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/WSingleNotification.qml @@ -18,6 +18,18 @@ MouseArea { signal groupExpandToggle hoverEnabled: true + function dismiss() { + Qt.callLater(() => { + Notifications.discardNotification(root.notification?.notificationId); + }); + removeAnimation.start(); + } + + WNotificationDismissAnim { + id: removeAnimation + target: root + } + implicitHeight: contentItem.implicitHeight implicitWidth: contentItem.implicitWidth @@ -25,9 +37,25 @@ MouseArea { animation: Looks.transition.enter.createObject(this) } + property real dragDismissThreshold: 100 + drag { + axis: Drag.XAxis + target: contentItem + minimumX: 0 + onActiveChanged: { + if (drag.active) + return; + if (contentItem.x > root.dragDismissThreshold) { + root.dismiss(); + } else { + contentItem.x = 0; + } + } + } + Rectangle { id: contentItem - anchors.fill: parent + width: parent.width color: Looks.colors.bgPanelBody radius: Looks.radius.medium property real padding: 12 @@ -36,6 +64,10 @@ MouseArea { border.width: 1 border.color: ColorUtils.applyAlpha(Looks.colors.ambientShadow, 0.1) + Behavior on x { + animation: Looks.transition.enter.createObject(this) + } + ColumnLayout { id: notificationContent anchors.fill: parent @@ -128,11 +160,7 @@ MouseArea { opacity: root.containsMouse ? 1 : 0 icon.name: "dismiss" implicitSize: 12 - onClicked: { - Qt.callLater(() => { - Notifications.discardNotification(root.notification?.notificationId); - }); - } + onClicked: root.dismiss() } } diff --git a/dots/.config/quickshell/ii/modules/waffle/startMenu/StartMenuContent.qml b/dots/.config/quickshell/ii/modules/waffle/startMenu/StartMenuContent.qml index fb506dd49..64ad321c6 100644 --- a/dots/.config/quickshell/ii/modules/waffle/startMenu/StartMenuContent.qml +++ b/dots/.config/quickshell/ii/modules/waffle/startMenu/StartMenuContent.qml @@ -86,7 +86,7 @@ WBarAttachedPanelContent { id: searchBar Layout.fillWidth: true implicitWidth: 832 // TODO: Make sizes naturally inferred - horizontalPadding: root.searching ? 24 : 32 + horizontalPadding: 32 // verticalPadding: root.searching ? 32 : 16 // TODO: make this not nuke the panel Synchronizer on searching { property alias target: root.searching diff --git a/dots/.config/quickshell/ii/modules/waffle/taskView/TaskViewContent.qml b/dots/.config/quickshell/ii/modules/waffle/taskView/TaskViewContent.qml index 7c7153fb9..4dc6f5bf7 100644 --- a/dots/.config/quickshell/ii/modules/waffle/taskView/TaskViewContent.qml +++ b/dots/.config/quickshell/ii/modules/waffle/taskView/TaskViewContent.qml @@ -1,44 +1,214 @@ import QtQuick +import QtQuick.Layouts import Quickshell +import Quickshell.Wayland +import Quickshell.Hyprland import qs import qs.services import qs.modules.common import qs.modules.common.functions +import qs.modules.common.models import qs.modules.common.widgets import qs.modules.waffle.looks +import "window-layout.js" as WindowLayout Rectangle { id: root color: ColorUtils.transparentize(Looks.colors.bg1Base, 0.5) + property bool draggingWindow: false property real openProgress: 0 + property Item hoveredWorkspace: null + signal closed Component.onCompleted: { openAnim.start(); } + function close() { + closeAnim.start(); + } PropertyAnimation { id: openAnim target: root property: "openProgress" to: 1 - duration: 200 + duration: 250 easing.type: Easing.BezierSpline easing.bezierCurve: Looks.transition.easing.bezierCurve.easeIn } - PropertyAnimation { + SequentialAnimation { id: closeAnim - target: root - property: "openProgress" - to: 0 - duration: 200 - easing.type: Easing.BezierSpline - easing.bezierCurve: Looks.transition.easing.bezierCurve.easeIn + + PropertyAnimation { + target: root + property: "openProgress" + to: 0 + duration: 250 + easing.type: Easing.BezierSpline + easing.bezierCurve: Looks.transition.easing.bezierCurve.easeIn + } + ScriptAction { + script: { + root.closed(); + } + } + } + + // Windows + property real maxWindowHeight: 290 + property real maxWindowWidth: 738 + property real padding: 52 + property real spacing: 25 + readonly property list toplevels: ToplevelManager.toplevels.values.filter(t => { + const client = HyprlandData.clientForToplevel(t); + return client && client.workspace.id === HyprlandData.activeWorkspace?.id; + }) + readonly property list arrangedToplevels: { + const maxRowWidth = width - padding * 2; + const count = toplevels.length; + const resultLayout = []; + + var i = 0; + while (i < count) { + var row = []; + var rowWidth = 0; + var j = i; + + while (j < count) { + const toplevel = toplevels[j]; + const client = HyprlandData.clientForToplevel(toplevel); + const scaledSize = WindowLayout.scaleWindow(client, maxWindowWidth, maxWindowHeight); + + if (rowWidth + scaledSize.width <= maxRowWidth || row.length === 0) { + row.push(toplevel); + rowWidth += scaledSize.width; + j++; + } else { + break; + } + } + + resultLayout.push(row); + i = j; + } + return resultLayout; + } + + MouseArea { + z: 0 + anchors.fill: parent + onClicked: { + GlobalStates.overviewOpen = false; + } + } + + // Windows + WListView { + id: windowListView + z: root.openProgress == 1 ? 2 : 1 + anchors { + left: parent.left + right: parent.right + top: parent.top + topMargin: (root.height - (wsBorder.height + 16) - height) / 2 + } + spacing: root.spacing + topMargin: root.padding + bottomMargin: root.padding + leftMargin: root.padding + rightMargin: root.padding + height: Math.min(contentHeight + topMargin + bottomMargin, root.height - (wsBorder.height + 16)) + + interactive: (height < contentHeight) && !root.draggingWindow + clip: root.openProgress > 0.99 && !root.draggingWindow + + model: ScriptModel { + values: root.arrangedToplevels + } + delegate: RowLayout { + id: clientRow + required property var modelData + spacing: root.spacing + anchors.horizontalCenter: parent?.horizontalCenter ?? undefined + + Repeater { + model: ScriptModel { + values: clientRow.modelData + } + delegate: Item { + id: clientGridArea + required property int index + required property var modelData + implicitWidth: windowItem.openedSize.width + implicitHeight: windowItem.openedSize.height + windowItem.titleBarImplicitHeight + + TaskViewWindow { + id: windowItem + z: Drag.active ? 2 : 1 + opacity: openAnim.running ? root.openProgress : 1 + + property int mappedX: { + // print("AAAWAWAAWAWWA: ", -(clientRow.x + clientGridArea.x + root.padding)); + var rootPosToThis = -(clientRow.x + clientGridArea.x + root.padding); + return rootPosToThis + hyprlandClient.at[0]; + } + property int mappedY: { + // print("AAAWAWAAWAWWA YYYY YUIUSDFOIU: ", clientRow.y + windowListView.y + root.padding + windowItem.titleBarImplicitHeight) + var rootPosToThis = -(clientRow.y + windowListView.y + root.padding + windowItem.titleBarImplicitHeight); + return rootPosToThis + hyprlandClient.at[1]; + } + property int openedX: 0 + property int openedY: 0 + // property int openedX: Drag.active ? (dragHandler.xAxis.activeValue) : 0 + // property int openedY: Drag.active ? (dragHandler.yAxis.activeValue) : 0 + scaleSize: (root.openProgress > 0 && !closeAnim.running) + x: mappedX + (openedX - mappedX) * root.openProgress + y: mappedY + (openedY - mappedY) * root.openProgress + + droppable: root.hoveredWorkspace !== null + Drag.active: dragHandler.active + Drag.hotSpot.x: mouseX + Drag.hotSpot.y: mouseY + + DragHandler { + id: dragHandler + target: null + xAxis.onActiveValueChanged: { + windowItem.openedX = dragHandler.xAxis.activeValue; + } + yAxis.onActiveValueChanged: { + windowItem.openedY = dragHandler.yAxis.activeValue; + } + onActiveChanged: { + if (active) { + root.draggingWindow = true; + } else { + root.draggingWindow = false; + if (root.hoveredWorkspace !== null && root.hoveredWorkspace.workspace !== windowItem.hyprlandClient.workspace.id) { + Hyprland.dispatch(`movetoworkspacesilent ${root.hoveredWorkspace.workspace}, address:${windowItem.hyprlandClient.address}`); + } else { + windowItem.openedX = 0; + windowItem.openedY = 0; + } + } + } + } + + Layout.alignment: Qt.AlignTop + maxHeight: root.maxWindowHeight + maxWidth: root.maxWindowWidth + toplevel: clientGridArea.modelData + } + } + } + } } // Workspaces Rectangle { id: wsBorder + z: root.openProgress == 1 ? 1 : 2 property real sourceEdgeMargin: -(height + 8) + root.openProgress * (height + 16) anchors { left: parent.left @@ -64,8 +234,9 @@ Rectangle { color: Looks.colors.bgPanelFooterBase implicitHeight: 174 - - ListView { + + WListView { + id: workspaceListView anchors { top: parent.top bottom: parent.bottom @@ -73,22 +244,56 @@ Rectangle { topMargin: 5 bottomMargin: 5 } + flickableDirection: Flickable.HorizontalFlick + orientation: ListView.Horizontal + interactive: width == parent.width width: Math.min(contentWidth + leftMargin + rightMargin, parent.width) leftMargin: 5 rightMargin: 5 clip: true - orientation: ListView.Horizontal spacing: 4 - model: ScriptModel { - values: { - const maxWorkspaceId = Math.max.apply(null, HyprlandData.workspaces.map(ws => ws.id)) - return Array(maxWorkspaceId) + function reposition() { + positionViewAtIndex(HyprlandData.activeWorkspace.id - 1, ListView.Contain); + } + + Connections { + target: HyprlandData + function onActiveWorkspaceChanged() { + workspaceListView.reposition(); + } + } + model: IndexModel { + id: workspaceIndexModel + count: { + const maxWorkspaceId = Math.max.apply(null, HyprlandData.workspaces.map(ws => ws.id)); + return Math.max(maxWorkspaceId, 1) + 1; } } delegate: TaskViewWorkspace { + id: workspaceItem required property int index workspace: index + 1 + newWorkspace: index == workspaceIndexModel.count - 1 + + droppable: root.hoveredWorkspace === workspaceItem + DropArea { + anchors.fill: parent + onEntered: drag => { + root.hoveredWorkspace = workspaceItem; + } + onExited: { + if (root.hoveredWorkspace === workspaceItem) { + root.hoveredWorkspace = null; + } + } + } + + onClicked: { + GlobalStates.overviewOpen = false; + root.closed(); // Close immediately to avoid weird animations + Hyprland.dispatch(`workspace ${workspaceItem.workspace}`); + } } } } diff --git a/dots/.config/quickshell/ii/modules/waffle/taskView/TaskViewWindow.qml b/dots/.config/quickshell/ii/modules/waffle/taskView/TaskViewWindow.qml new file mode 100644 index 000000000..0cab91fe5 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/taskView/TaskViewWindow.qml @@ -0,0 +1,155 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Qt5Compat.GraphicalEffects +import Quickshell +import Quickshell.Wayland +import Quickshell.Hyprland +import qs +import qs.services +import qs.modules.common +import qs.modules.common.functions +import qs.modules.common.widgets +import qs.modules.waffle.looks +import "window-layout.js" as WindowLayout + +WMouseAreaButton { + id: root + + required property var toplevel + required property int maxHeight + required property int maxWidth + + property var hyprlandClient: HyprlandData.clientForToplevel(root.toplevel) + property string address: hyprlandClient?.address + + property string iconName: AppSearch.guessIcon(hyprlandClient?.class) + + color: drag.active ? ColorUtils.transparentize(Looks.colors.bg1Base) : (containsMouse ? Looks.colors.bg1Base : Looks.colors.bgPanelFooterBase) + borderColor: ColorUtils.transparentize(Looks.colors.bg2Border, drag.active ? 1 : 0) + radius: Looks.radius.xLarge + + property real titleBarImplicitHeight: titleBar.implicitHeight + property bool scaleSize: true + property size openedSize: WindowLayout.scaleWindow(hyprlandClient, maxWidth, maxHeight); + property size fullSize: Qt.size(hyprlandClient?.size[0] ?? maxWidth, hyprlandClient?.size[1] ?? maxHeight) + property size size: scaleSize ? openedSize : fullSize + implicitWidth: Math.max(Math.round(contentItem.implicitWidth), 138) + implicitHeight: Math.round(contentItem.implicitHeight) + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Item { + width: root.background.width + height: root.background.height + Rectangle { + radius: root.background.radius + anchors { + fill: parent + topMargin: root.drag.active ? root.titleBarImplicitHeight : 0 + } + } + } + } + property bool droppable: false + scale: (root.pressedButtons & Qt.LeftButton || root.Drag.active) ? (droppable ? 0.4 : 0.95) : 1 + Behavior on scale { + NumberAnimation { + id: scaleAnim + duration: 200 + easing.type: Easing.OutExpo + } + } + + function closeWindow() { + Hyprland.dispatch(`closewindow address:${root.hyprlandClient?.address}`); + } + + acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton + onClicked: event => { + if (event.button === Qt.LeftButton) { + GlobalStates.overviewOpen = false; + Hyprland.dispatch(`focuswindow address:${root.hyprlandClient?.address}`); + GlobalStates.overviewOpen = false; + } else if (event.button === Qt.MiddleButton) { + root.closeWindow(); + event.accepted = true; + } else if (event.button === Qt.RightButton) { + if (!windowMenu.visible) + windowMenu.popup(); + else + windowMenu.close(); + } + } + + ColumnLayout { + id: contentItem + z: 2 + anchors.fill: parent + anchors.margins: 1 + spacing: 0 + + RowLayout { + id: titleBar + opacity: root.drag.active ? 0 : 1 + spacing: 8 + WAppIcon { + Layout.leftMargin: 10 + Layout.alignment: Qt.AlignVCenter + iconName: root.iconName + implicitSize: 16 + tryCustomIcon: false + } + WText { + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + elide: Text.ElideRight + text: root.hyprlandClient?.title ?? "" + } + CloseButton { + implicitWidth: 38 + implicitHeight: 38 + padding: 8 + onClicked: root.closeWindow() + } + } + + ScreencopyView { + Layout.fillHeight: true + Layout.alignment: Qt.AlignHCenter + implicitWidth: Math.round(root.size.width) + implicitHeight: Math.round(root.size.height) + constraintSize: Qt.size(Math.round(root.size.width), Math.round(root.size.height)) + + Behavior on implicitWidth { + animation: Looks.transition.enter.createObject(this) + } + Behavior on implicitHeight { + animation: Looks.transition.enter.createObject(this) + } + + captureSource: root.toplevel ?? null + live: true + } + } + + WMenu { + id: windowMenu + downDirection: true + + Action { + enabled: root.hyprlandClient?.floating + property bool isPinned: root.hyprlandClient?.pinned + icon.name: isPinned ? "checkmark" : "empty" + text: Translation.tr("Show this window on all desktops") + onTriggered: { + Hyprland.dispatch(`pin address:${root.hyprlandClient?.address}`); + } + } + Action { + icon.name: "empty" + text: Translation.tr("Close") + onTriggered: root.closeWindow() + } + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/taskView/TaskViewWorkspace.qml b/dots/.config/quickshell/ii/modules/waffle/taskView/TaskViewWorkspace.qml index 987511f05..8fe3f35bd 100644 --- a/dots/.config/quickshell/ii/modules/waffle/taskView/TaskViewWorkspace.qml +++ b/dots/.config/quickshell/ii/modules/waffle/taskView/TaskViewWorkspace.qml @@ -15,22 +15,37 @@ WMouseAreaButton { id: root required property int workspace + property bool newWorkspace: false + property bool droppable: false - readonly property real screenWidth: QsWindow.window.width - readonly property real screenHeight: QsWindow.window.height + readonly property bool isActiveWorkspace: HyprlandData.activeWorkspace?.id === root.workspace + readonly property real screenWidth: QsWindow.window?.width ?? 0 + readonly property real screenHeight: QsWindow.window?.height ?? 0 readonly property real screenAspectRatio: screenWidth / screenHeight - readonly property real screenScale: QsWindow.window.devicePixelRatio - readonly property real scale: 0.1148148148 + readonly property real windowScale: wallpaperHeight / screenHeight - height: ListView.view.height + property real wallpaperHeight: 124 + + height: ListView.view?.height ?? 100 implicitWidth: 244 // for now - onClicked: { - GlobalStates.overviewOpen = false; - Hyprland.dispatch(`workspace ${root.workspace}`); + colBackground: ColorUtils.transparentize(Looks.colors.bg2, (isActiveWorkspace || droppable) ? 0 : 1) + Behavior on color { + animation: Looks.transition.color.createObject(this) } + scale: root.containsPress ? 0.95 : 1 + Behavior on scale { + NumberAnimation { + id: scaleAnim + duration: 300 + easing.type: Easing.OutExpo + } + } + + // Content ColumnLayout { + id: contentItem anchors { fill: parent leftMargin: 12 @@ -45,15 +60,15 @@ WMouseAreaButton { Layout.fillHeight: false horizontalAlignment: Text.AlignLeft elide: Text.ElideRight - text: Translation.tr("Desktop %1").arg(root.workspace) + text: root.newWorkspace ? Translation.tr("New desktop") : Translation.tr("Desktop %1").arg(root.workspace) } Rectangle { id: wsBg - height: 124 + height: root.wallpaperHeight Layout.fillHeight: true Layout.fillWidth: true - color: Looks.colors.bg1Base + color: Looks.colors.bg1 layer.enabled: true layer.effect: OpacityMask { @@ -64,34 +79,67 @@ WMouseAreaButton { } } - StyledImage { + // Workspace content + Loader { anchors.fill: parent - cache: true - sourceSize: Qt.size(root.screenAspectRatio * 124, 124) - source: Config.options.background.wallpaperPath - fillMode: Image.PreserveAspectCrop + active: !root.newWorkspace + sourceComponent: StyledImage { + cache: true + sourceSize: Qt.size(root.screenAspectRatio * root.wallpaperHeight, root.wallpaperHeight) + source: Config.options.background.wallpaperPath + fillMode: Image.PreserveAspectCrop - Repeater { - model: ScriptModel { - values: ToplevelManager.toplevels.values.filter(toplevel => { - const address = `0x${toplevel.HyprlandToplevel?.address}`; - var win = HyprlandData.windowByAddress[address]; - const inWorkspace = win?.workspace?.id === root.workspace; - return inWorkspace; - }) - } - delegate: ScreencopyView { - required property var modelData - readonly property var hyprlandWindowData: HyprlandData.windowByAddress[`0x${modelData.HyprlandToplevel?.address}`] - captureSource: modelData - live: true - width: hyprlandWindowData?.size[0] * root.scale - height: hyprlandWindowData?.size[1] * root.scale - x: hyprlandWindowData?.at[0] * root.scale - y: hyprlandWindowData?.at[1] * root.scale + Repeater { + model: ScriptModel { + values: HyprlandData.toplevelsForWorkspace(root.workspace) + } + delegate: ScreencopyView { + required property var modelData + readonly property var hyprlandWindowData: HyprlandData.windowByAddress[`0x${modelData.HyprlandToplevel?.address}`] + captureSource: modelData + live: true + width: hyprlandWindowData?.size[0] * root.windowScale + height: hyprlandWindowData?.size[1] * root.windowScale + x: hyprlandWindowData?.at[0] * root.windowScale + y: hyprlandWindowData?.at[1] * root.windowScale + } } } } + + // New plus icon + Loader { + anchors.centerIn: parent + active: root.newWorkspace + sourceComponent: FluentIcon { + icon: "add" + } + } + + Rectangle { + z: 2 + visible: root.droppable && !root.newWorkspace + anchors.fill: parent + color: Looks.colors.accent + opacity: 0.2 + } + } + } + + // Active indicator + WFadeLoader { + anchors { + horizontalCenter: parent.horizontalCenter + bottom: parent.bottom + } + shown: root.isActiveWorkspace + + sourceComponent: Rectangle { + id: activeIndicator + implicitWidth: 32 + implicitHeight: 3 + color: Looks.colors.accent + radius: height / 2 } } } diff --git a/dots/.config/quickshell/ii/modules/waffle/taskView/WaffleTaskView.qml b/dots/.config/quickshell/ii/modules/waffle/taskView/WaffleTaskView.qml index 75e71ccb1..05a2ccf40 100644 --- a/dots/.config/quickshell/ii/modules/waffle/taskView/WaffleTaskView.qml +++ b/dots/.config/quickshell/ii/modules/waffle/taskView/WaffleTaskView.qml @@ -23,7 +23,14 @@ Scope { Loader { id: panelLoader required property var modelData - active: GlobalStates.overviewOpen + active: false + Connections { + target: GlobalStates + function onOverviewOpenChanged() { + if (GlobalStates.overviewOpen) + panelLoader.active = true; + } + } sourceComponent: PanelWindow { id: root property string searchingText: "" @@ -33,7 +40,7 @@ Scope { WlrLayershell.namespace: "quickshell:wTaskView" WlrLayershell.layer: WlrLayer.Overlay - // WlrLayershell.keyboardFocus: GlobalStates.overviewOpen ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None + WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand color: "transparent" anchors { @@ -44,7 +51,26 @@ Scope { } TaskViewContent { + id: taskViewContent anchors.fill: parent + + Component.onCompleted: { + taskViewContent.forceActiveFocus(); + } + Keys.onPressed: event => { + if (event.key === Qt.Key_Escape) { + GlobalStates.overviewOpen = false; + } + } + + Connections { + target: GlobalStates + function onOverviewOpenChanged() { + if (!GlobalStates.overviewOpen) + taskViewContent.close(); + } + } + onClosed: panelLoader.active = false } } } diff --git a/dots/.config/quickshell/ii/modules/waffle/taskView/window-layout.js b/dots/.config/quickshell/ii/modules/waffle/taskView/window-layout.js new file mode 100644 index 000000000..d5548bdb5 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/taskView/window-layout.js @@ -0,0 +1,36 @@ +function scaleWindow(hyprlandClient, maxWindowWidth, maxWindowHeight) { + const [width, height] = hyprlandClient.size; + const [xScale, yScale] = [maxWindowWidth / width, maxWindowHeight / height]; + const scale = Math.min(xScale, yScale); + return Qt.size(width * scale, height * scale) +} + +function arrangedClients(hyprlandClients, maxRowWidth, maxWindowWidth, maxWindowHeight) { + const count = hyprlandClients.length; + const resultLayout = []; + + var i = 0; + while (i < count) { + var row = []; + var rowWidth = 0; + var j = i; + + while (j < count) { + const client = hyprlandClients[j]; + const scaledSize = scaleWindow(client, maxWindowWidth, maxWindowHeight); + + if (rowWidth + scaledSize.width <= maxRowWidth || row.length === 0) { + row.push(client); + rowWidth += scaledSize.width; + j++; + } else { + break; + } + } + + resultLayout.push(row); + i = j; + } + + return resultLayout; +} diff --git a/dots/.config/quickshell/ii/scripts/thumbnails/thumbgen-venv.sh b/dots/.config/quickshell/ii/scripts/thumbnails/thumbgen-venv.sh index fc7d8729e..5b24f16f9 100755 --- a/dots/.config/quickshell/ii/scripts/thumbnails/thumbgen-venv.sh +++ b/dots/.config/quickshell/ii/scripts/thumbnails/thumbgen-venv.sh @@ -2,6 +2,6 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source $(eval echo $ILLOGICAL_IMPULSE_VIRTUAL_ENV)/bin/activate -"$SCRIPT_DIR/thumbgen.py" "$@" +GIO_USE_VFS=local "$SCRIPT_DIR/thumbgen.py" "$@" deactivate diff --git a/dots/.config/quickshell/ii/scripts/thumbnails/thumbgen.py b/dots/.config/quickshell/ii/scripts/thumbnails/thumbgen.py index 69f630721..92dd126f6 100755 --- a/dots/.config/quickshell/ii/scripts/thumbnails/thumbgen.py +++ b/dots/.config/quickshell/ii/scripts/thumbnails/thumbgen.py @@ -15,7 +15,7 @@ import gi from loguru import logger from tqdm import tqdm -gi.require_version("GnomeDesktop", "3.0") +gi.require_version("GnomeDesktop", "4.0") from gi.repository import Gio, GnomeDesktop # isort:skip thumbnail_size_map = { diff --git a/dots/.config/quickshell/ii/services/HyprlandData.qml b/dots/.config/quickshell/ii/services/HyprlandData.qml index abbaaf577..5ec7bd68d 100644 --- a/dots/.config/quickshell/ii/services/HyprlandData.qml +++ b/dots/.config/quickshell/ii/services/HyprlandData.qml @@ -4,6 +4,7 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell import Quickshell.Io +import Quickshell.Wayland import Quickshell.Hyprland /** @@ -21,6 +22,30 @@ Singleton { property var monitors: [] property var layers: ({}) + // Convenient stuff + + function toplevelsForWorkspace(workspace) { + return ToplevelManager.toplevels.values.filter(toplevel => { + const address = `0x${toplevel.HyprlandToplevel?.address}`; + var win = HyprlandData.windowByAddress[address]; + return win?.workspace?.id === workspace; + }) + } + + function hyprlandClientsForWorkspace(workspace) { + return root.windowList.filter(win => win.workspace.id === workspace); + } + + function clientForToplevel(toplevel) { + if (!toplevel || !toplevel.HyprlandToplevel) { + return null; + } + const address = `0x${toplevel?.HyprlandToplevel?.address}`; + return root.windowByAddress[address]; + } + + // Internals + function updateWindowList() { getClients.running = true; } @@ -63,6 +88,7 @@ Singleton { function onRawEvent(event) { // console.log("Hyprland raw event:", event.name); + if (["openlayer", "closelayer", "screencast"].includes(event.name)) return; updateAll() } } diff --git a/dots/.config/quickshell/ii/services/LauncherSearch.qml b/dots/.config/quickshell/ii/services/LauncherSearch.qml index f5a18bec9..52e783e28 100644 --- a/dots/.config/quickshell/ii/services/LauncherSearch.qml +++ b/dots/.config/quickshell/ii/services/LauncherSearch.qml @@ -4,6 +4,7 @@ import qs.modules.common import qs.modules.common.models import qs.modules.common.functions import QtQuick +import Qt.labs.folderlistmodel import Quickshell import Quickshell.Io @@ -31,6 +32,34 @@ Singleton { return acc; }, []).sort() + // Load user action scripts from ~/.config/illogical-impulse/actions/ + // Uses FolderListModel to auto-reload when scripts are added/removed + property var userActionScripts: { + const actions = []; + for (let i = 0; i < userActionsFolder.count; i++) { + const fileName = userActionsFolder.get(i, "fileName"); + const filePath = userActionsFolder.get(i, "filePath"); + if (fileName && filePath) { + const actionName = fileName.replace(/\.[^/.]+$/, ""); // strip extension + actions.push({ + action: actionName, + execute: ((path) => (args) => { + Quickshell.execDetached([path, ...(args ? args.split(" ") : [])]); + })(FileUtils.trimFileProtocol(filePath.toString())) + }); + } + } + return actions; + } + + FolderListModel { + id: userActionsFolder + folder: Qt.resolvedUrl(Directories.userActions) + showDirs: false + showHidden: false + sortField: FolderListModel.Name + } + property var searchActions: [ { action: "accentcolor", @@ -90,6 +119,9 @@ Singleton { }, ] + // Combined built-in and user actions + property var allActions: searchActions.concat(userActionScripts) + property string mathResult: "" property bool clipboardWorkSafetyActive: { const enabled = Config.options.workSafety.enable.clipboard; @@ -273,7 +305,7 @@ Singleton { Qt.openUrlExternally(url); } }); - const launcherActionObjects = root.searchActions.map(action => { + const launcherActionObjects = root.allActions.map(action => { const actionString = `${Config.options.search.prefix.action}${action.action}`; if (actionString.startsWith(root.query) || root.query.startsWith(actionString)) { return resultComp.createObject(null, { diff --git a/dots/.config/quickshell/ii/services/ai/GeminiApiStrategy.qml b/dots/.config/quickshell/ii/services/ai/GeminiApiStrategy.qml index b610c3d44..9f04f6364 100644 --- a/dots/.config/quickshell/ii/services/ai/GeminiApiStrategy.qml +++ b/dots/.config/quickshell/ii/services/ai/GeminiApiStrategy.qml @@ -111,6 +111,14 @@ ApiStrategy { return ({}) } + // Error response handling + if (dataJson.error) { + const errorMsg = `**Error ${dataJson.error.code}**: ${dataJson.error.message}`; + message.rawContent += errorMsg; + message.content += errorMsg; + return { finished: true }; + } + // No candidates? if (!dataJson.candidates) return {}; diff --git a/dots/.config/quickshell/ii/services/ai/MistralApiStrategy.qml b/dots/.config/quickshell/ii/services/ai/MistralApiStrategy.qml index 14cd1a17a..9a37df096 100644 --- a/dots/.config/quickshell/ii/services/ai/MistralApiStrategy.qml +++ b/dots/.config/quickshell/ii/services/ai/MistralApiStrategy.qml @@ -58,8 +58,17 @@ ApiStrategy { // Real stuff try { const dataJson = JSON.parse(cleanData); + + // Error response handling + if (dataJson.error) { + const errorMsg = `**Error**: ${dataJson.error.message || JSON.stringify(dataJson.error)}`; + message.rawContent += errorMsg; + message.content += errorMsg; + return { finished: true }; + } + let newContent = ""; - + const responseContent = dataJson.choices[0]?.delta?.content || dataJson.message?.content; const responseReasoning = dataJson.choices[0]?.delta?.reasoning || dataJson.choices[0]?.delta?.reasoning_content; diff --git a/dots/.config/quickshell/ii/services/ai/OpenAiApiStrategy.qml b/dots/.config/quickshell/ii/services/ai/OpenAiApiStrategy.qml index 1178c837f..a049abf30 100644 --- a/dots/.config/quickshell/ii/services/ai/OpenAiApiStrategy.qml +++ b/dots/.config/quickshell/ii/services/ai/OpenAiApiStrategy.qml @@ -49,8 +49,17 @@ ApiStrategy { // Real stuff try { const dataJson = JSON.parse(cleanData); + + // Error response handling + if (dataJson.error) { + const errorMsg = `**Error**: ${dataJson.error.message || JSON.stringify(dataJson.error)}`; + message.rawContent += errorMsg; + message.content += errorMsg; + return { finished: true }; + } + let newContent = ""; - + const responseContent = dataJson.choices[0]?.delta?.content || dataJson.message?.content; const responseReasoning = dataJson.choices[0]?.delta?.reasoning || dataJson.choices[0]?.delta?.reasoning_content; diff --git a/sdata/dist-fedora/SPECS/quickshell-git.spec b/sdata/dist-fedora/SPECS/quickshell-git.spec index e90bdebe2..5167a1ae3 100644 --- a/sdata/dist-fedora/SPECS/quickshell-git.spec +++ b/sdata/dist-fedora/SPECS/quickshell-git.spec @@ -63,8 +63,6 @@ Wayland and X11. %endif -DBUILD_SHARED_LIBS=OFF \ -DCMAKE_BUILD_TYPE=Release \ - -DDISTRIBUTOR="Fedora COPR (errornointernet/quickshell)" \ - -DDISTRIBUTOR_DEBUGINFO_AVAILABLE=YES \ -DGIT_REVISION=%{commit} \ -DINSTALL_QML_PREFIX=%{_lib}/qt6/qml %cmake_build diff --git a/sdata/dist-fedora/feddeps.toml b/sdata/dist-fedora/feddeps.toml index 367626ced..def14e066 100644 --- a/sdata/dist-fedora/feddeps.toml +++ b/sdata/dist-fedora/feddeps.toml @@ -1,3 +1,13 @@ +# COPR repositories list +[copr] +repos = [ + "ririko66z/dots-hyprland", + "solopasha/hyprland", + "deltacopy/darkly", + "alternateved/eza", + "atim/starship" +] + # Fedora dependencies list # Audio @@ -17,6 +27,7 @@ packages = [ "brightnessctl", "ddcutil" ] +install_opts = ["--setopt=install_weak_deps=False"] # Basic [groups.basic] diff --git a/sdata/dist-fedora/install-deps.sh b/sdata/dist-fedora/install-deps.sh index 20e1d1328..18da73e06 100644 --- a/sdata/dist-fedora/install-deps.sh +++ b/sdata/dist-fedora/install-deps.sh @@ -1,9 +1,15 @@ # This script is meant to be sourced. # It's not for directly running. +# ------------------------- +# CONFIG +# ------------------------- +user_config="${REPO_ROOT}/sdata/dist-fedora/user_data.yaml" +rpmbuildroot="${REPO_ROOT}/cache/rpmbuild" +deps_data_file="${REPO_ROOT}/sdata/dist-fedora/feddeps.toml" - -# Initialize the user configuration file -user_config=${REPO_ROOT}/sdata/dist-fedora/user_data.yaml +# ------------------------- +# FUNCTIONS +# ------------------------- # Recording DNF Transaction ID function r() { @@ -16,6 +22,42 @@ function r() { [ "$original_id" == "$last_id" ] || yq -i ".dnf.transaction_ids += [ $last_id ]" "$user_config" || : } +# Start building and install the missing RPM package locally. +function install_RPMS() { + local local_specs local_rpms + rpmbuildroot="${rpmbuildroot:-${REPO_ROOT}/cache/rpmbuild}" + + x mkdir -p "$rpmbuildroot"/{BUILD,RPMS,SOURCES} + x cp -r "${REPO_ROOT}/sdata/dist-fedora/SPECS" "$rpmbuildroot/" + + x cd $rpmbuildroot/SPECS + + mapfile -t -d '' local_specs < <(find "$rpmbuildroot/SPECS" -maxdepth 1 -type f -name "*.spec" -print0) + for spec_file in ${local_specs[@]}; do + # Download sources + x spectool -g -C "$rpmbuildroot/SOURCES" "$spec_file" + # Install build dependencies + r x sudo dnf builddep -y "$spec_file" + # Build the RPM package locally. If it fails, download it from COPR. + if ! rpmbuild -bb --define "_topdir $rpmbuildroot" --define "debug_package %{nil}" "$spec_file"; then + printf "${STY_RED}Local build encountered an issue. Downloading $(basename "$spec_file" .spec) from COPR. Report the issue to Discussions pls.${STY_RST}\n" + sudo dnf install -y $(basename "$spec_file" .spec) + nolock_qs=true + fi + done + + mapfile -t -d '' local_rpms < <(find "$rpmbuildroot/RPMS" -maxdepth 2 -type f -name '*.rpm' -not -name '*debug*' -print0) + if [[ ${#local_rpms[@]} -ge 1 ]]; then + echo -e "${STY_BLUE}Next command:${STY_RST} sudo dnf install ${local_rpms[@]} -y" + r x sudo dnf install "${local_rpms[@]}" -y + fi + x cd ${REPO_ROOT} +} + +# ------------------------- +# MAIN +# ------------------------- + if ! command -v dnf >/dev/null 2>&1; then printf "${STY_RED}[$0]: dnf not found, it seems that the system is not Fedora 42 or later distros. Aborting...${STY_RST}\n" exit 1 @@ -33,61 +75,38 @@ v sudo dnf versionlock delete quickshell-git 2>/dev/null # Install yq for parsing config files v sudo dnf install yq -y -# Development-tools -r v sudo dnf install @development-tools fedora-packager rpmdevtools fonts-rpm-macros qt6-rpm-macros -y +# Install development tools +r v sudo dnf install @development-tools fedora-packager -y -# COPR repositories -v sudo dnf copr enable ririko66z/dots-hyprland -y -v sudo dnf copr enable solopasha/hyprland -y -v sudo dnf copr enable deltacopy/darkly -y -v sudo dnf copr enable alternateved/eza -y -v sudo dnf copr enable atim/starship -y - -# Start building and install the missing RPM package locally. -install_RPMS() { - rpmbuildroot=${REPO_ROOT}/cache/rpmbuild - x mkdir -p $rpmbuildroot/{BUILD,RPMS,SOURCES} - x cp -r ${REPO_ROOT}/sdata/dist-fedora/SPECS $rpmbuildroot/ - x cd $rpmbuildroot/SPECS - mapfile -t -d '' local_specs < <(find "$rpmbuildroot/SPECS" -maxdepth 1 -type f -name "*.spec" -print0) - for spec_file in ${local_specs[@]}; do - x spectool -g -C "$rpmbuildroot/SOURCES" $spec_file - r x sudo dnf builddep -y $spec_file - x rpmbuild -bb --define "_topdir $rpmbuildroot" $spec_file - done - mapfile -t -d '' local_rpms < <(find "$rpmbuildroot/RPMS" -maxdepth 2 -type f -name '*.rpm' -not -name '*debug*' -print0) - echo -e "${STY_BLUE}Next command:${STY_RST} sudo dnf install ${local_rpms[@]} -y" - r x sudo dnf install "${local_rpms[@]}" -y - x cd ${REPO_ROOT} -} +# Install COPR repositories +copr_repos_json=$(yq -o=j '.copr.repos // []' "$deps_data_file") +eval "$(jq -r '@sh "copr_repos_array+=(\(.[]))"' <<<"$copr_repos_json")" # Fedora distro contains jq +for copr in ${copr_repos_array[@]}; do + v sudo dnf copr enable "$copr" -y +done +# Build and install locally RPMS showfun install_RPMS v install_RPMS -deps_data_file="${REPO_ROOT}/sdata/dist-fedora/feddeps.toml" +# Install packages from toml file deps_data=$(yq -o=j '.' "$deps_data_file") echo "Starting to install packages from $deps_data_file ..." while IFS= read -r deps_list_key; do - echo "Installing package list: $deps_list_key" + install_opts=$(echo $deps_data | yq ".groups.\"$deps_list_key\" | select(has(\"install_opts\")) | .install_opts[]") package_list=$(echo $deps_data | yq ".groups.\"$deps_list_key\".packages | unique | .[]") r v sudo dnf install -y $install_opts $package_list 0) -') +done < <(echo "$deps_data" | yq '.groups | keys[]? | select(length > 0)') # Add back versionlock at the end -v sudo dnf versionlock add quickshell-git +[ -n $nolock_qs ] || v sudo dnf versionlock add quickshell-git || true echo -e "\n========================================" -echo "All installations are complete." +echo "All installations are completed." echo "========================================" - diff --git a/sdata/dist-gentoo/README.md b/sdata/dist-gentoo/README.md index 630a3a178..84153c779 100644 --- a/sdata/dist-gentoo/README.md +++ b/sdata/dist-gentoo/README.md @@ -52,3 +52,29 @@ end - The Hyprland live ebuild sometimes has linkage issues, deleting _Hyprland_ and _hyprland_ from `/usr/bin/` and then re-emerging usually fixes this. - When emerging Hyprland if you get an issue relating to `undefined reference to ``Hyprutils::Math::Vector2D::˜Vector2D()`` ` - Clear the cache folder (`rm -fr /var/tmp/portage/gui-wm/hyprland*`) then try again +- If emerging ``hyprland-qtutils`` fails and gives you something like this... + ```cmake + CMake Error at utils/dialog/CMakeLists.txt:26 (target_link_libraries): + Target "hyprland-dialog" links to: + Qt6::WaylandClientPrivate + but the target was not found. Possible reasons include: + * There is a typo in the target name. + * A find_package call is missing for an IMPORTED target. + * An ALIAS target is missing. + CMake Error at utils/update-screen/CMakeLists.txt:34 (target_link_libraries): + Target "hyprland-update-screen" links to: + Qt6::WaylandClientPrivate + but the target was not found. Possible reasons include: + * There is a typo in the target name. + * A find_package call is missing for an IMPORTED target. + * An ALIAS target is missing. + CMake Error at utils/donate-screen/CMakeLists.txt:32 (target_link_libraries): + Target "hyprland-donate-screen" links to: + Qt6::WaylandClientPrivate + but the target was not found. Possible reasons include: + * There is a typo in the target name. + * A find_package call is missing for an IMPORTED target. + * An ALIAS target is missing. + ``` + Try putting ``sdata/dist-gentoo/hyprland-qtutils-private.patch`` into ``/etc/portage/patches/gui-libs/hyprland-qtutils/``. + - Patch Credit: fedeliallalinea on https://forums.gentoo.org/viewtopic-p-8874098.html diff --git a/sdata/dist-gentoo/hyprland-qtutils-private.patch b/sdata/dist-gentoo/hyprland-qtutils-private.patch new file mode 100644 index 000000000..6ca55104f --- /dev/null +++ b/sdata/dist-gentoo/hyprland-qtutils-private.patch @@ -0,0 +1,33 @@ +--- a/utils/dialog/CMakeLists.txt ++++ b/utils/dialog/CMakeLists.txt +@@ -8,7 +8,7 @@ + set(CMAKE_CXX_STANDARD 23) + set(CMAKE_CXX_STANDARD_REQUIRED ON) + +-find_package(Qt6 6.5 REQUIRED COMPONENTS Widgets Quick QuickControls2 WaylandClient) ++find_package(Qt6 6.5 REQUIRED COMPONENTS Widgets Quick QuickControls2 WaylandClient WaylandClientPrivate) + find_package(PkgConfig REQUIRED) + + pkg_check_modules(hyprutils REQUIRED IMPORTED_TARGET hyprutils) +--- a/utils/donate-screen/CMakeLists.txt ++++ b/utils/donate-screen/CMakeLists.txt +@@ -8,7 +8,7 @@ + set(CMAKE_CXX_STANDARD 23) + set(CMAKE_CXX_STANDARD_REQUIRED ON) + +-find_package(Qt6 6.5 REQUIRED COMPONENTS Widgets Quick QuickControls2 WaylandClient) ++find_package(Qt6 6.5 REQUIRED COMPONENTS Widgets Quick QuickControls2 WaylandClient WaylandClientPrivate) + find_package(PkgConfig REQUIRED) + + pkg_check_modules(hyprutils REQUIRED IMPORTED_TARGET hyprutils) +--- a/utils/update-screen/CMakeLists.txt ++++ b/utils/update-screen/CMakeLists.txt +@@ -8,7 +8,7 @@ + set(CMAKE_CXX_STANDARD 23) + set(CMAKE_CXX_STANDARD_REQUIRED ON) + +-find_package(Qt6 6.5 REQUIRED COMPONENTS Widgets Quick QuickControls2 WaylandClient) ++find_package(Qt6 6.5 REQUIRED COMPONENTS Widgets Quick QuickControls2 WaylandClient WaylandClientPrivate) + find_package(PkgConfig REQUIRED) + + pkg_check_modules(hyprutils REQUIRED IMPORTED_TARGET hyprutils) diff --git a/sdata/dist-gentoo/install-deps.sh b/sdata/dist-gentoo/install-deps.sh index 0aadbf81b..9d79c157a 100644 --- a/sdata/dist-gentoo/install-deps.sh +++ b/sdata/dist-gentoo/install-deps.sh @@ -1,23 +1,12 @@ printf "${STY_YELLOW}" printf "============WARNING/NOTE (1)============\n" -printf "GCC in use: $(which gcc)\n" -printf "GCC version info: $(gcc --version | grep gcc)\n" -printf "GCC version number: $(gcc --version | grep gcc | awk '{print $3}')\n" -printf "GCC-15>= is required for Hyprland\n" -printf "If you have GCC-15>= and it's currently set then you can safely ignore this\n" -printf "If not, you must ensure you are using the correct GCC version and set it (gcc-config )\n" -printf "It is heavily recommended to re-emerge @world with an empty tree after changing GCC version (emerge -e @world)\n\n" -printf "${STY_RST}" -pause - -printf "${STY_YELLOW}" -printf "============WARNING/NOTE (2)============\n" printf "Ensure you have a global use flag for elogind or systemd in your make.conf for simplicity\n" printf "Or you can manually add the use flags for each package that requires it\n" printf "${STY_RST}" pause printf "${STY_YELLOW}" +printf "============WARNING/NOTE (2)============\n" printf "https://github.com/end-4/dots-hyprland/blob/main/sdata/dist-gentoo/README.md\n" printf "Checkout the above README for potential bug fixes or additional information\n\n" printf "${STY_RST}" diff --git a/sdata/lib/functions.sh b/sdata/lib/functions.sh index 0fe24f0ed..423f45b81 100644 --- a/sdata/lib/functions.sh +++ b/sdata/lib/functions.sh @@ -64,7 +64,7 @@ function showfun(){ function pause(){ if [ ! "$ask" == "false" ];then printf "${STY_FAINT}${STY_SLANT}" - local p; read -p "(Ctrl-C to abort, others to proceed)" p + local p; read -p "(Ctrl-C to abort, Enter to proceed)" p printf "${STY_RST}" fi } diff --git a/sdata/subcmd-install/3.files-exp.yaml b/sdata/subcmd-install/3.files-exp.yaml index 5282e0594..63a0cad1f 100644 --- a/sdata/subcmd-install/3.files-exp.yaml +++ b/sdata/subcmd-install/3.files-exp.yaml @@ -20,6 +20,7 @@ patterns: - from: "dots/.config/fish" to: "$XDG_CONFIG_HOME/fish" mode: "sync" + excludes: ["conf.d"] condition: type: "shell" value: "fish" diff --git a/sdata/subcmd-install/3.files-legacy.sh b/sdata/subcmd-install/3.files-legacy.sh index 6ba8cefc2..0a3bef85f 100644 --- a/sdata/subcmd-install/3.files-legacy.sh +++ b/sdata/subcmd-install/3.files-legacy.sh @@ -30,7 +30,7 @@ esac case "${SKIP_FISH}" in true) sleep 0;; *) - install_dir__sync dots/.config/fish "$XDG_CONFIG_HOME"/fish + install_dir__sync_exclude dots/.config/fish "$XDG_CONFIG_HOME"/fish "conf.d" ;; esac diff --git a/sdata/subcmd-install/3.files.sh b/sdata/subcmd-install/3.files.sh index 41e9972d0..57c8568e1 100644 --- a/sdata/subcmd-install/3.files.sh +++ b/sdata/subcmd-install/3.files.sh @@ -67,6 +67,22 @@ rsync_dir__sync(){ x mkdir -p "$(dirname ${INSTALLED_LISTFILE})" rsync -a --delete --out-format='%i %n' "$1"/ "$2"/ | awk -v d="$dest" '$1 ~ /^>/{ sub(/^[^ ]+ /,""); printf d "/" $0 "\n" }' >> "${INSTALLED_LISTFILE}" } +rsync_dir__sync_exclude(){ + # NOTE: This function is only for using in other functions + # Same as rsync_dir__sync but with exclude patterns support + # Usage: rsync_dir__sync_exclude [ ...] + local src="$1" + local dest_dir="$2" + shift 2 + local excludes=() + for pattern in "$@"; do + excludes+=(--exclude "$pattern") + done + x mkdir -p "$dest_dir" + local dest="$(realpath -se $dest_dir)" + x mkdir -p "$(dirname ${INSTALLED_LISTFILE})" + rsync -a --delete "${excludes[@]}" --out-format='%i %n' "$src"/ "$dest_dir"/ | awk -v d="$dest" '$1 ~ /^>/{ sub(/^[^ ]+ /,""); printf d "/" $0 "\n" }' >> "${INSTALLED_LISTFILE}" +} function install_file(){ # NOTE: Do not add prefix `v` or `x` when using this function local s=$1 @@ -124,6 +140,18 @@ function install_dir__skip_existed(){ v rsync_dir $s $t fi } +function install_dir__sync_exclude(){ + # NOTE: Do not add prefix `v` or `x` when using this function + # Sync directory with exclude patterns + # Usage: install_dir__sync_exclude [ ...] + local s=$1 + local t=$2 + shift 2 + if [ -d $t ];then + warning_overwrite + fi + rsync_dir__sync_exclude $s $t "$@" +} function install_google_sans_flex(){ local font_name="Google Sans Flex" local src_name="google-sans-flex" @@ -142,7 +170,7 @@ function install_google_sans_flex(){ x fc-cache -fv x cd $REPO_ROOT x mkdir -p "$(dirname ${INSTALLED_LISTFILE})" - realpath -se "$2" >> "${INSTALLED_LISTFILE}" + realpath -se "$target_dir" >> "${INSTALLED_LISTFILE}" } #####################################################################################