Merge branch 'end-4:main' into main

This commit is contained in:
jwihardi
2025-11-07 01:28:10 -05:00
committed by GitHub
80 changed files with 2369 additions and 1307 deletions
+1
View File
@@ -28,6 +28,7 @@
<details> <details>
<summary>Installation (illogical-impulse Quickshell)</summary> <summary>Installation (illogical-impulse Quickshell)</summary>
- _If you're new to Linux and decide to use Hyprland, you're in for a tough ride._
- Just run `bash <(curl -s https://ii.clsty.link/get)` - Just run `bash <(curl -s https://ii.clsty.link/get)`
- Or, clone this repo and run `./setup install` - Or, clone this repo and run `./setup install`
- See [document](https://ii.clsty.link/en/ii-qs/01setup/) for details. - See [document](https://ii.clsty.link/en/ii-qs/01setup/) for details.
@@ -2,8 +2,11 @@ name: Comment on Discussion When sdata/dist-arch/ Changes
on: on:
push: push:
branches:
- main
paths: paths:
- 'sdata/dist-arch/**' - "sdata/dist-arch/**"
- "!sdata/dist-arch/README.md"
# workflow_dispatch: # workflow_dispatch:
jobs: jobs:
+1 -1
View File
@@ -1,4 +1,4 @@
$lock_cmd = swaylock $lock_cmd = swaylock -c 000000
# $lock_cmd = pidof hyprlock || hyprlock # $lock_cmd = pidof hyprlock || hyprlock
$suspend_cmd = systemctl suspend || loginctl suspend $suspend_cmd = systemctl suspend || loginctl suspend
+1 -1
View File
@@ -33,7 +33,7 @@ bindd = Super, N, Toggle right sidebar, global, quickshell:sidebarRightToggle #
bindd = Super, Slash, Toggle cheatsheet, global, quickshell:cheatsheetToggle # Toggle cheatsheet bindd = Super, Slash, Toggle cheatsheet, global, quickshell:cheatsheetToggle # Toggle cheatsheet
bindd = Super, K, Toggle on-screen keyboard, global, quickshell:oskToggle # Toggle on-screen keyboard bindd = Super, K, Toggle on-screen keyboard, global, quickshell:oskToggle # Toggle on-screen keyboard
bindd = Super, M, Toggle media controls, global, quickshell:mediaControlsToggle # Toggle media controls bindd = Super, M, Toggle media controls, global, quickshell:mediaControlsToggle # Toggle media controls
bind = Super, G, global, quickshell:crosshairToggle # Toggle crosshair bind = Super, G, global, quickshell:overlayToggle # Toggle overlay
bindd = Ctrl+Alt, Delete, Toggle session menu, global, quickshell:sessionToggle # Toggle session menu bindd = Ctrl+Alt, Delete, Toggle session menu, global, quickshell:sessionToggle # Toggle session menu
bindd = Super, J, Toggle bar, global, quickshell:barToggle # Toggle bar bindd = Super, J, Toggle bar, global, quickshell:barToggle # Toggle bar
bind = Ctrl+Alt, Delete, exec, qs -c $qsConfig ipc call TEST_ALIVE || pkill wlogout || wlogout -p layer-shell # [hidden] Session menu (fallback) bind = Ctrl+Alt, Delete, exec, qs -c $qsConfig ipc call TEST_ALIVE || pkill wlogout || wlogout -p layer-shell # [hidden] Session menu (fallback)
+1 -1
View File
@@ -134,11 +134,11 @@ layerrule = blur, quickshell:.*
layerrule = ignorealpha 0.79, quickshell:.* layerrule = ignorealpha 0.79, quickshell:.*
layerrule = animation slide, quickshell:bar layerrule = animation slide, quickshell:bar
layerrule = animation slide bottom, quickshell:cheatsheet layerrule = animation slide bottom, quickshell:cheatsheet
layerrule = noanim, quickshell:crosshair
layerrule = animation slide bottom, quickshell:dock layerrule = animation slide bottom, quickshell:dock
layerrule = animation popin 120%, quickshell:screenCorners layerrule = animation popin 120%, quickshell:screenCorners
layerrule = noanim, quickshell:lockWindowPusher layerrule = noanim, quickshell:lockWindowPusher
layerrule = animation fade, quickshell:notificationPopup layerrule = animation fade, quickshell:notificationPopup
layerrule = noanim, quickshell:overlay
layerrule = noanim, quickshell:overview layerrule = noanim, quickshell:overview
layerrule = animation slide bottom, quickshell:osk layerrule = animation slide bottom, quickshell:osk
layerrule = noanim, quickshell:polkit layerrule = noanim, quickshell:polkit
@@ -17,6 +17,7 @@ Singleton {
property bool osdBrightnessOpen: false property bool osdBrightnessOpen: false
property bool osdVolumeOpen: false property bool osdVolumeOpen: false
property bool oskOpen: false property bool oskOpen: false
property bool overlayOpen: false
property bool overviewOpen: false property bool overviewOpen: false
property bool regionSelectorOpen: false property bool regionSelectorOpen: false
property bool screenLocked: false property bool screenLocked: false
@@ -4,6 +4,7 @@ import qs
import qs.services import qs.services
import qs.modules.common import qs.modules.common
import qs.modules.common.widgets import qs.modules.common.widgets
import qs.modules.common.widgets.widgetCanvas
import qs.modules.common.functions as CF import qs.modules.common.functions as CF
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
@@ -13,18 +14,12 @@ import Quickshell.Io
import Quickshell.Wayland import Quickshell.Wayland
import Quickshell.Hyprland import Quickshell.Hyprland
import qs.modules.background.cookieClock import qs.modules.background.widgets
import qs.modules.background.widgets.clock
import qs.modules.background.widgets.weather
Variants { Variants {
id: root id: root
readonly property bool fixedClockPosition: Config.options.background.clock.fixedPosition
readonly property real fixedClockX: Config.options.background.clock.x
readonly property real fixedClockY: Config.options.background.clock.y
readonly property real clockSizePadding: 20
readonly property real screenSizePadding: 50
readonly property string clockStyle: Config.options.background.clock.style
readonly property bool showCookieQuote: Config.options.background.showQuote && Config.options.background.quote !== "" && !GlobalStates.screenLocked && Config.options.background.clock.style === "cookie"
readonly property real clockParallaxFactor: Config.options.background.parallax.clockFactor // 0 = full parallax, 1 = no parallax
model: Quickshell.screens model: Quickshell.screens
PanelWindow { PanelWindow {
@@ -46,9 +41,9 @@ Variants {
property bool wallpaperIsVideo: Config.options.background.wallpaperPath.endsWith(".mp4") || Config.options.background.wallpaperPath.endsWith(".webm") || Config.options.background.wallpaperPath.endsWith(".mkv") || Config.options.background.wallpaperPath.endsWith(".avi") || Config.options.background.wallpaperPath.endsWith(".mov") property bool wallpaperIsVideo: Config.options.background.wallpaperPath.endsWith(".mp4") || Config.options.background.wallpaperPath.endsWith(".webm") || Config.options.background.wallpaperPath.endsWith(".mkv") || Config.options.background.wallpaperPath.endsWith(".avi") || Config.options.background.wallpaperPath.endsWith(".mov")
property string wallpaperPath: wallpaperIsVideo ? Config.options.background.thumbnailPath : Config.options.background.wallpaperPath property string wallpaperPath: wallpaperIsVideo ? Config.options.background.thumbnailPath : Config.options.background.wallpaperPath
property bool wallpaperSafetyTriggered: { property bool wallpaperSafetyTriggered: {
const enabled = Config.options.workSafety.enable.wallpaper const enabled = Config.options.workSafety.enable.wallpaper;
const sensitiveWallpaper = (CF.StringUtils.stringListContainsSubstring(wallpaperPath.toLowerCase(), Config.options.workSafety.triggerCondition.fileKeywords)) const sensitiveWallpaper = (CF.StringUtils.stringListContainsSubstring(wallpaperPath.toLowerCase(), Config.options.workSafety.triggerCondition.fileKeywords));
const sensitiveNetwork = (CF.StringUtils.stringListContainsSubstring(Network.networkName.toLowerCase(), Config.options.workSafety.triggerCondition.networkNameKeywords)) const sensitiveNetwork = (CF.StringUtils.stringListContainsSubstring(Network.networkName.toLowerCase(), Config.options.workSafety.triggerCondition.networkNameKeywords));
return enabled && sensitiveWallpaper && sensitiveNetwork; return enabled && sensitiveWallpaper && sensitiveNetwork;
} }
property real wallpaperToScreenRatio: Math.min(wallpaperWidth / screen.width, wallpaperHeight / screen.height) property real wallpaperToScreenRatio: Math.min(wallpaperWidth / screen.width, wallpaperHeight / screen.height)
@@ -59,18 +54,6 @@ Variants {
property real movableXSpace: ((wallpaperWidth / wallpaperToScreenRatio * effectiveWallpaperScale) - screen.width) / 2 property real movableXSpace: ((wallpaperWidth / wallpaperToScreenRatio * effectiveWallpaperScale) - screen.width) / 2
property real movableYSpace: ((wallpaperHeight / wallpaperToScreenRatio * effectiveWallpaperScale) - screen.height) / 2 property real movableYSpace: ((wallpaperHeight / wallpaperToScreenRatio * effectiveWallpaperScale) - screen.height) / 2
readonly property bool verticalParallax: (Config.options.background.parallax.autoVertical && wallpaperHeight > wallpaperWidth) || Config.options.background.parallax.vertical readonly property bool verticalParallax: (Config.options.background.parallax.autoVertical && wallpaperHeight > wallpaperWidth) || Config.options.background.parallax.vertical
// Position
property real clockX: (modelData.width / 2)
property real clockY: (modelData.height / 2)
property var textHorizontalAlignment: {
if ((Config.options.lock.centerClock && GlobalStates.screenLocked) || wallpaperSafetyTriggered)
return Text.AlignHCenter;
if (clockX < screen.width / 3)
return Text.AlignLeft;
if (clockX > screen.width * 2 / 3)
return Text.AlignRight;
return Text.AlignHCenter;
}
// Colors // Colors
property bool shouldBlur: (GlobalStates.screenLocked && Config.options.lock.blur.enable) property bool shouldBlur: (GlobalStates.screenLocked && Config.options.lock.blur.enable)
property color dominantColor: Appearance.colors.colPrimary // Default, to be changed property color dominantColor: Appearance.colors.colPrimary // Default, to be changed
@@ -97,8 +80,9 @@ Variants {
right: true right: true
} }
color: { color: {
if (!bgRoot.wallpaperSafetyTriggered || bgRoot.wallpaperIsVideo) return "transparent"; if (!bgRoot.wallpaperSafetyTriggered || bgRoot.wallpaperIsVideo)
return CF.ColorUtils.mix(Appearance.colors.colLayer0, Appearance.colors.colPrimary, 0.75) return "transparent";
return CF.ColorUtils.mix(Appearance.colors.colLayer0, Appearance.colors.colPrimary, 0.75);
} }
Behavior on color { Behavior on color {
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this) animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
@@ -134,53 +118,15 @@ Variants {
// Oversized = can be zoomed for parallax, yay // Oversized = can be zoomed for parallax, yay
bgRoot.effectiveWallpaperScale = Math.min(bgRoot.preferredWallpaperScale, width / screenWidth, height / screenHeight); bgRoot.effectiveWallpaperScale = Math.min(bgRoot.preferredWallpaperScale, width / screenWidth, height / screenHeight);
} }
bgRoot.updateClockPosition();
} }
} }
} }
// Clock positioning
function updateClockPosition() {
// Somehow all this manual setting is needed to make the proc correctly use the new values
leastBusyRegionProc.path = bgRoot.wallpaperPath;
leastBusyRegionProc.contentWidth = clockLoader.implicitWidth + root.clockSizePadding * 2;
leastBusyRegionProc.contentHeight = clockLoader.implicitHeight + root.clockSizePadding * 2;
leastBusyRegionProc.horizontalPadding = bgRoot.movableXSpace + root.screenSizePadding * 2;
leastBusyRegionProc.verticalPadding = bgRoot.movableYSpace + root.screenSizePadding * 2;
leastBusyRegionProc.running = false;
leastBusyRegionProc.running = true;
}
Process {
id: leastBusyRegionProc
property string path: bgRoot.wallpaperPath
property int contentWidth: 300
property int contentHeight: 300
property int horizontalPadding: bgRoot.movableXSpace
property int verticalPadding: bgRoot.movableYSpace
command: [Quickshell.shellPath("scripts/images/least-busy-region-venv.sh"), "--screen-width", Math.round(bgRoot.screen.width / bgRoot.effectiveWallpaperScale), "--screen-height", Math.round(bgRoot.screen.height / bgRoot.effectiveWallpaperScale), "--width", contentWidth, "--height", contentHeight, "--horizontal-padding", horizontalPadding, "--vertical-padding", verticalPadding, path
// "--visual-output",
,]
stdout: StdioCollector {
id: leastBusyRegionOutputCollector
onStreamFinished: {
const output = leastBusyRegionOutputCollector.text;
// console.log("[Background] Least busy region output:", output)
if (output.length === 0)
return;
const parsedContent = JSON.parse(output);
bgRoot.clockX = parsedContent.center_x * bgRoot.effectiveWallpaperScale;
bgRoot.clockY = parsedContent.center_y * bgRoot.effectiveWallpaperScale;
bgRoot.dominantColor = parsedContent.dominant_color || Appearance.colors.colPrimary;
}
}
}
// Wallpaper
Item { Item {
anchors.fill: parent anchors.fill: parent
clip: true clip: true
// Wallpaper
StyledImage { StyledImage {
id: wallpaper id: wallpaper
visible: opacity > 0 && !blurLoader.active visible: opacity > 0 && !blurLoader.active
@@ -261,25 +207,23 @@ Variants {
} }
} }
// The clock WidgetCanvas {
Loader { id: widgetCanvas
id: clockLoader
scale: Config.options.background.clock.scale
active: Config.options.background.clock.show
anchors { anchors {
left: wallpaper.left left: wallpaper.left
right: wallpaper.right
top: wallpaper.top top: wallpaper.top
horizontalCenter: undefined bottom: wallpaper.bottom
verticalCenter: undefined readonly property real parallaxFactor: Config.options.background.parallax.widgetsFactor
leftMargin: { leftMargin: {
const clockXOnWallpaper = bgRoot.movableXSpace + ((root.fixedClockPosition ? root.fixedClockX : bgRoot.clockX * bgRoot.effectiveWallpaperScale) - implicitWidth / 2) const xOnWallpaper = bgRoot.movableXSpace;
const extraMove = (wallpaper.effectiveValueX * 2 * bgRoot.movableXSpace) * (root.clockParallaxFactor - 1); const extraMove = (wallpaper.effectiveValueX * 2 * bgRoot.movableXSpace) * (parallaxFactor - 1);
return clockXOnWallpaper - extraMove; return xOnWallpaper - extraMove;
} }
topMargin: { topMargin: {
const clockYOnWallpaper = bgRoot.movableYSpace + ((root.fixedClockPosition ? root.fixedClockY : bgRoot.clockY * bgRoot.effectiveWallpaperScale) - implicitHeight / 2) const yOnWallpaper = bgRoot.movableYSpace;
const extraMove = (wallpaper.effectiveValueY * 2 * bgRoot.movableYSpace) * (root.clockParallaxFactor - 1); const extraMove = (wallpaper.effectiveValueY * 2 * bgRoot.movableYSpace) * (parallaxFactor - 1);
return clockYOnWallpaper - extraMove; return yOnWallpaper - extraMove;
} }
Behavior on leftMargin { Behavior on leftMargin {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this) animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
@@ -288,193 +232,65 @@ Variants {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this) animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
} }
} }
width: wallpaper.width
height: wallpaper.height
states: State { states: State {
name: "centered" name: "centered"
when: (GlobalStates.screenLocked && Config.options.lock.centerClock) || bgRoot.wallpaperSafetyTriggered when: GlobalStates.screenLocked || bgRoot.wallpaperSafetyTriggered
PropertyChanges {
target: widgetCanvas
width: parent.width
height: parent.height
}
AnchorChanges { AnchorChanges {
target: clockLoader target: widgetCanvas
anchors { anchors {
left: undefined left: undefined
right: undefined right: undefined
top: undefined top: undefined
verticalCenter: parent.verticalCenter bottom: undefined
horizontalCenter: parent.horizontalCenter // horizontalCenter: parent.horizontalCenter
// verticalCenter: parent.verticalCenter
} }
} }
} }
transitions: Transition { transitions: Transition {
PropertyAnimation {
properties: "width,height"
duration: Appearance.animation.elementMove.duration
easing.type: Appearance.animation.elementMove.type
easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
}
AnchorAnimation { AnchorAnimation {
duration: Appearance.animation.elementMove.duration duration: Appearance.animation.elementMove.duration
easing.type: Appearance.animation.elementMove.type easing.type: Appearance.animation.elementMove.type
easing.bezierCurve: Appearance.animation.elementMove.bezierCurve easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
} }
} }
sourceComponent: Column {
Loader {
id: digitalClockLoader
visible: root.clockStyle === "digital"
active: visible
sourceComponent: ColumnLayout {
id: clockColumn
spacing: 6
ClockText { FadeLoader {
font.pixelSize: 90 shown: Config.options.background.widgets.weather.enable
text: DateTime.time sourceComponent: WeatherWidget {
} screenWidth: bgRoot.screen.width
ClockText { screenHeight: bgRoot.screen.height
Layout.topMargin: -5 scaledScreenWidth: bgRoot.screen.width / bgRoot.effectiveWallpaperScale
text: DateTime.date scaledScreenHeight: bgRoot.screen.height / bgRoot.effectiveWallpaperScale
} wallpaperScale: bgRoot.effectiveWallpaperScale
StyledText {
// Somehow gets fucked up if made a ClockText???
visible: Config.options.background.showQuote && Config.options.background.quote.length > 0
Layout.fillWidth: true
horizontalAlignment: bgRoot.textHorizontalAlignment
font {
pixelSize: Appearance.font.pixelSize.normal
weight: 350
}
color: bgRoot.colText
style: Text.Raised
styleColor: Appearance.colors.colShadow
text: Config.options.background.quote
}
}
} }
Loader {
id: cookieClockLoader
visible: root.clockStyle === "cookie"
active: visible
sourceComponent: CookieClock {}
}
Loader {
id: cookieQuoteLoader
visible: root.showCookieQuote
active: visible
sourceComponent: CookieQuote {}
anchors.horizontalCenter: cookieClockLoader.horizontalCenter
}
} }
Item { FadeLoader {
anchors { shown: Config.options.background.widgets.clock.enable
top: clockLoader.bottom sourceComponent: ClockWidget {
topMargin: 8 screenWidth: bgRoot.screen.width
horizontalCenter: (bgRoot.textHorizontalAlignment === Text.AlignHCenter || root.clockStyle === "cookie") ? clockLoader.horizontalCenter : undefined screenHeight: bgRoot.screen.height
left: (bgRoot.textHorizontalAlignment === Text.AlignLeft) ? clockLoader.left : undefined scaledScreenWidth: bgRoot.screen.width / bgRoot.effectiveWallpaperScale
right: (bgRoot.textHorizontalAlignment === Text.AlignRight) ? clockLoader.right : undefined scaledScreenHeight: bgRoot.screen.height / bgRoot.effectiveWallpaperScale
leftMargin: -26 wallpaperScale: bgRoot.effectiveWallpaperScale
rightMargin: -26 wallpaperSafetyTriggered: bgRoot.wallpaperSafetyTriggered
}
implicitWidth: statusTextBg.implicitWidth
implicitHeight: statusTextBg.implicitHeight
StyledRectangularShadow {
target: statusTextBg
visible: statusTextBg.visible && root.clockStyle === "cookie"
opacity: statusTextBg.opacity
}
Rectangle {
id: statusTextBg
anchors.centerIn: parent
clip: true
opacity: (safetyStatusText.shown || lockStatusText.shown) ? 1 : 0
visible: opacity > 0
implicitHeight: statusTextRow.implicitHeight + 5 * 2
implicitWidth: statusTextRow.implicitWidth + 5 * 2
radius: Appearance.rounding.small
color: CF.ColorUtils.transparentize(Appearance.colors.colSecondaryContainer, root.clockStyle === "cookie" ? 0 : 1)
Behavior on implicitWidth {
animation: Appearance.animation.elementResize.numberAnimation.createObject(this)
}
Behavior on implicitHeight {
animation: Appearance.animation.elementResize.numberAnimation.createObject(this)
}
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
RowLayout {
id: statusTextRow
anchors.centerIn: parent
spacing: 14
Item {
Layout.fillWidth: bgRoot.textHorizontalAlignment !== Text.AlignLeft
implicitWidth: 1
}
ClockStatusText {
id: safetyStatusText
shown: bgRoot.wallpaperSafetyTriggered
statusIcon: "hide_image"
statusText: Translation.tr("Wallpaper safety enforced")
}
ClockStatusText {
id: lockStatusText
shown: GlobalStates.screenLocked && Config.options.lock.showLockedText
statusIcon: "lock"
statusText: Translation.tr("Locked")
}
Item {
Layout.fillWidth: bgRoot.textHorizontalAlignment !== Text.AlignRight
implicitWidth: 1
}
}
} }
} }
} }
} }
} }
// ComponentsCookieClock {}
component ClockText: StyledText {
Layout.fillWidth: true
horizontalAlignment: bgRoot.textHorizontalAlignment
font {
family: Appearance.font.family.expressive
pixelSize: 20
weight: Font.DemiBold
}
color: bgRoot.colText
style: Text.Raised
styleColor: Appearance.colors.colShadow
animateChange: Config.options.background.clock.digital.animateChange
}
component ClockStatusText: Row {
id: statusTextRow
property alias statusIcon: statusIconWidget.text
property alias statusText: statusTextWidget.text
property bool shown: true
property color textColor: root.clockStyle === "cookie" ? Appearance.colors.colOnSecondaryContainer : bgRoot.colText
opacity: shown ? 1 : 0
visible: opacity > 0
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
spacing: 4
MaterialSymbol {
id: statusIconWidget
anchors.verticalCenter: statusTextRow.verticalCenter
iconSize: Appearance.font.pixelSize.huge
color: statusTextRow.textColor
style: Text.Raised
styleColor: Appearance.colors.colShadow
}
ClockText {
id: statusTextWidget
color: statusTextRow.textColor
anchors.verticalCenter: statusTextRow.verticalCenter
font {
pixelSize: Appearance.font.pixelSize.large
weight: Font.Normal
}
style: Text.Raised
styleColor: Appearance.colors.colShadow
}
}
} }
@@ -0,0 +1,101 @@
import QtQuick
import Quickshell
import Quickshell.Io
import qs
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets.widgetCanvas
AbstractWidget {
id: root
required property string configEntryName
required property int screenWidth
required property int screenHeight
required property int scaledScreenWidth
required property int scaledScreenHeight
required property real wallpaperScale
property bool visibleWhenLocked: false
property var configEntry: Config.options.background.widgets[configEntryName]
property string placementStrategy: configEntry.placementStrategy
property real targetX: Math.max(0, Math.min(configEntry.x, scaledScreenWidth - width))
property real targetY : Math.max(0, Math.min(configEntry.y, scaledScreenHeight - height))
x: targetX
y: targetY
visible: opacity > 0
opacity: (GlobalStates.screenLocked && !visibleWhenLocked) ? 0 : 1
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
scale: (draggable && containsPress) ? 1.05 : 1
Behavior on scale {
animation: Appearance.animation.elementResize.numberAnimation.createObject(this)
}
draggable: placementStrategy === "free"
onReleased: {
root.targetX = root.x;
root.targetY = root.y;
configEntry.x = root.targetX;
configEntry.y = root.targetY;
}
property bool needsColText: false
property color dominantColor: Appearance.colors.colPrimary
property bool dominantColorIsDark: dominantColor.hslLightness < 0.5
property color colText: {
const onNormalBackground = (GlobalStates.screenLocked && Config.options.lock.blur.enable)
const adaptiveColor = ColorUtils.colorWithLightness(Appearance.colors.colPrimary, (dominantColorIsDark ? 0.8 : 0.12))
return onNormalBackground ? Appearance.colors.colOnLayer0 : adaptiveColor;
}
property bool wallpaperIsVideo: Config.options.background.wallpaperPath.endsWith(".mp4") || Config.options.background.wallpaperPath.endsWith(".webm") || Config.options.background.wallpaperPath.endsWith(".mkv") || Config.options.background.wallpaperPath.endsWith(".avi") || Config.options.background.wallpaperPath.endsWith(".mov")
property string wallpaperPath: wallpaperIsVideo ? Config.options.background.thumbnailPath : Config.options.background.wallpaperPath
onWallpaperPathChanged: refreshPlacementIfNeeded()
onPlacementStrategyChanged: refreshPlacementIfNeeded()
Connections {
target: Config
function onReadyChanged() { refreshPlacementIfNeeded() }
}
function refreshPlacementIfNeeded() {
if (!Config.ready || (root.placementStrategy === "free" && root.needsColText)) return;
leastBusyRegionProc.wallpaperPath = root.wallpaperPath;
leastBusyRegionProc.running = false;
leastBusyRegionProc.running = true;
}
Process {
id: leastBusyRegionProc
property string wallpaperPath: root.wallpaperPath
// TODO: make these less arbitrary
property int contentWidth: 300
property int contentHeight: 300
property int horizontalPadding: 200
property int verticalPadding: 200
command: [Quickshell.shellPath("scripts/images/least-busy-region-venv.sh") // Comments to force the formatter to break lines
, "--screen-width", Math.round(root.scaledScreenWidth) //
, "--screen-height", Math.round(root.scaledScreenHeight) //
, "--width", contentWidth //
, "--height", contentHeight //
, "--horizontal-padding", horizontalPadding //
, "--vertical-padding", verticalPadding //
, wallpaperPath //
, ...(root.placementStrategy === "mostBusy" ? ["--busiest"] : [])
// "--visual-output",
]
stdout: StdioCollector {
id: leastBusyRegionOutputCollector
onStreamFinished: {
const output = leastBusyRegionOutputCollector.text;
// console.log("[Background] Least busy region output:", output)
if (output.length === 0) return;
const parsedContent = JSON.parse(output);
root.dominantColor = parsedContent.dominant_color || Appearance.colors.colPrimary;
if (root.placementStrategy === "free") return;
root.targetX = parsedContent.center_x * root.wallpaperScale - root.width / 2;
root.targetY = parsedContent.center_y * root.wallpaperScale - root.height / 2;
}
}
}
}
@@ -0,0 +1,195 @@
import QtQuick
import QtQuick.Layouts
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.modules.common.widgets.widgetCanvas
import qs.modules.background.widgets
AbstractBackgroundWidget {
id: root
configEntryName: "clock"
implicitHeight: contentColumn.implicitHeight
implicitWidth: contentColumn.implicitWidth
property string clockStyle: Config.options.background.widgets.clock.style
property bool forceCenter: (GlobalStates.screenLocked && Config.options.lock.centerClock)
property bool wallpaperSafetyTriggered: false
needsColText: clockStyle === "digital"
x: forceCenter ? ((root.screenWidth - root.width) / 2) : targetX
y: forceCenter ? ((root.screenHeight - root.height) / 2) : targetY
visibleWhenLocked: true
property var textHorizontalAlignment: {
if (root.forceCenter)
return Text.AlignHCenter;
if (root.x < root.scaledScreenWidth / 3)
return Text.AlignLeft;
if (root.x > root.scaledScreenWidth * 2 / 3)
return Text.AlignRight;
return Text.AlignHCenter;
}
Column {
id: contentColumn
anchors.centerIn: parent
spacing: 6
FadeLoader {
id: cookieClockLoader
anchors.horizontalCenter: parent.horizontalCenter
shown: root.clockStyle === "cookie"
sourceComponent: Column {
CookieClock {
anchors.horizontalCenter: parent.horizontalCenter
}
FadeLoader {
anchors.horizontalCenter: parent.horizontalCenter
shown: Config.options.background.widgets.clock.quote.enable && Config.options.background.widgets.clock.quote.text !== ""
sourceComponent: CookieQuote {}
}
}
}
FadeLoader {
id: digitalClockLoader
anchors.horizontalCenter: parent.horizontalCenter
shown: root.clockStyle === "digital"
sourceComponent: ColumnLayout {
id: clockColumn
spacing: 6
ClockText {
font.pixelSize: 90
text: DateTime.time
}
ClockText {
Layout.topMargin: -5
text: DateTime.date
}
StyledText {
// Somehow gets fucked up if made a ClockText???
visible: Config.options.background.widgets.clock.quote.enable && Config.options.background.widgets.clock.quote.text.length > 0
Layout.fillWidth: true
horizontalAlignment: root.textHorizontalAlignment
font {
pixelSize: Appearance.font.pixelSize.normal
weight: 350
}
color: root.colText
style: Text.Raised
styleColor: Appearance.colors.colShadow
text: Config.options.background.widgets.clock.quote.text
}
}
}
Item {
id: statusText
anchors.horizontalCenter: parent.horizontalCenter
implicitHeight: statusTextBg.implicitHeight
implicitWidth: statusTextBg.implicitWidth
StyledRectangularShadow {
target: statusTextBg
visible: statusTextBg.visible && root.clockStyle === "cookie"
opacity: statusTextBg.opacity
}
Rectangle {
id: statusTextBg
anchors.centerIn: parent
clip: true
opacity: (safetyStatusText.shown || lockStatusText.shown) ? 1 : 0
visible: opacity > 0
implicitHeight: statusTextRow.implicitHeight + 5 * 2
implicitWidth: statusTextRow.implicitWidth + 5 * 2
radius: Appearance.rounding.small
color: ColorUtils.transparentize(Appearance.colors.colSecondaryContainer, root.clockStyle === "cookie" ? 0 : 1)
Behavior on implicitWidth {
animation: Appearance.animation.elementResize.numberAnimation.createObject(this)
}
Behavior on implicitHeight {
animation: Appearance.animation.elementResize.numberAnimation.createObject(this)
}
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
RowLayout {
id: statusTextRow
anchors.centerIn: parent
spacing: 14
Item {
Layout.fillWidth: root.textHorizontalAlignment !== Text.AlignLeft
implicitWidth: 1
}
ClockStatusText {
id: safetyStatusText
shown: root.wallpaperSafetyTriggered
statusIcon: "hide_image"
statusText: Translation.tr("Wallpaper safety enforced")
}
ClockStatusText {
id: lockStatusText
shown: GlobalStates.screenLocked && Config.options.lock.showLockedText
statusIcon: "lock"
statusText: Translation.tr("Locked")
}
Item {
Layout.fillWidth: root.textHorizontalAlignment !== Text.AlignRight
implicitWidth: 1
}
}
}
}
}
component ClockText: StyledText {
Layout.fillWidth: true
horizontalAlignment: root.textHorizontalAlignment
font {
family: Appearance.font.family.expressive
pixelSize: 20
weight: Font.DemiBold
}
color: root.colText
style: Text.Raised
styleColor: Appearance.colors.colShadow
animateChange: Config.options.background.widgets.clock.digital.animateChange
}
component ClockStatusText: Row {
id: statusTextRow
property alias statusIcon: statusIconWidget.text
property alias statusText: statusTextWidget.text
property bool shown: true
property color textColor: root.clockStyle === "cookie" ? Appearance.colors.colOnSecondaryContainer : root.colText
opacity: shown ? 1 : 0
visible: opacity > 0
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
spacing: 4
MaterialSymbol {
id: statusIconWidget
anchors.verticalCenter: statusTextRow.verticalCenter
iconSize: Appearance.font.pixelSize.huge
color: statusTextRow.textColor
style: Text.Raised
styleColor: Appearance.colors.colShadow
}
ClockText {
id: statusTextWidget
color: statusTextRow.textColor
anchors.verticalCenter: statusTextRow.verticalCenter
font {
pixelSize: Appearance.font.pixelSize.large
weight: Font.Normal
}
style: Text.Raised
styleColor: Appearance.colors.colShadow
}
}
}
@@ -9,13 +9,13 @@ import QtQuick.Layouts
import Qt5Compat.GraphicalEffects import Qt5Compat.GraphicalEffects
import Quickshell.Io import Quickshell.Io
import qs.modules.background.cookieClock.dateIndicator import qs.modules.background.widgets.clock.dateIndicator
import qs.modules.background.cookieClock.minuteMarks import qs.modules.background.widgets.clock.minuteMarks
Item { Item {
id: root id: root
readonly property string clockStyle: Config.options.background.clock.style readonly property string clockStyle: Config.options.background.widgets.clock.style
property real implicitSize: 230 property real implicitSize: 230
@@ -36,16 +36,16 @@ Item {
implicitHeight: implicitSize implicitHeight: implicitSize
function applyStyle(sides, dialStyle, hourHandStyle, minuteHandStyle, secondHandStyle, dateStyle) { function applyStyle(sides, dialStyle, hourHandStyle, minuteHandStyle, secondHandStyle, dateStyle) {
Config.options.background.clock.cookie.sides = sides Config.options.background.widgets.clock.cookie.sides = sides
Config.options.background.clock.cookie.dialNumberStyle = dialStyle Config.options.background.widgets.clock.cookie.dialNumberStyle = dialStyle
Config.options.background.clock.cookie.hourHandStyle = hourHandStyle Config.options.background.widgets.clock.cookie.hourHandStyle = hourHandStyle
Config.options.background.clock.cookie.minuteHandStyle = minuteHandStyle Config.options.background.widgets.clock.cookie.minuteHandStyle = minuteHandStyle
Config.options.background.clock.cookie.secondHandStyle = secondHandStyle Config.options.background.widgets.clock.cookie.secondHandStyle = secondHandStyle
Config.options.background.clock.cookie.dateStyle = dateStyle Config.options.background.widgets.clock.cookie.dateStyle = dateStyle
} }
function setClockPreset(category) { function setClockPreset(category) {
if (!Config.options.background.clock.cookie.aiStyling) return; if (!Config.options.background.widgets.clock.cookie.aiStyling) return;
if (category === "") return; if (category === "") return;
print("[Cookie clock] Setting clock preset for category: " + category) print("[Cookie clock] Setting clock preset for category: " + category)
// "abstract", "anime", "city", "minimalist", "landscape", "plants", "person", "space" // "abstract", "anime", "city", "minimalist", "landscape", "plants", "person", "space"
@@ -83,17 +83,12 @@ Item {
} }
} }
property bool useSineCookie: Config.options.background.clock.cookie.useSineCookie property bool useSineCookie: Config.options.background.widgets.clock.cookie.useSineCookie
DropShadow { StyledDropShadow {
source: useSineCookie ? sineCookieLoader : roundedPolygonCookieLoader target: useSineCookie ? sineCookieLoader : roundedPolygonCookieLoader
anchors.fill: source
radius: 8
samples: radius * 2 + 1
color: root.colShadow
transparentBorder: true
RotationAnimation on rotation { RotationAnimation on rotation {
running: Config.options.background.clock.cookie.constantlyRotate running: Config.options.background.widgets.clock.cookie.constantlyRotate
duration: 30000 duration: 30000
easing.type: Easing.Linear easing.type: Easing.Linear
loops: Animation.Infinite loops: Animation.Infinite
@@ -108,7 +103,7 @@ Item {
active: useSineCookie active: useSineCookie
sourceComponent: SineCookie { sourceComponent: SineCookie {
implicitSize: root.implicitSize implicitSize: root.implicitSize
sides: Config.options.background.clock.cookie.sides sides: Config.options.background.widgets.clock.cookie.sides
color: root.colBackground color: root.colBackground
} }
} }
@@ -119,7 +114,7 @@ Item {
active: !useSineCookie active: !useSineCookie
sourceComponent: MaterialCookie { sourceComponent: MaterialCookie {
implicitSize: root.implicitSize implicitSize: root.implicitSize
sides: Config.options.background.clock.cookie.sides sides: Config.options.background.widgets.clock.cookie.sides
color: root.colBackground color: root.colBackground
} }
} }
@@ -134,7 +129,7 @@ Item {
FadeLoader { FadeLoader {
id: hourMarksLoader id: hourMarksLoader
anchors.centerIn: parent anchors.centerIn: parent
shown: Config.options.background.clock.cookie.hourMarks shown: Config.options.background.widgets.clock.cookie.hourMarks
sourceComponent: HourMarks { sourceComponent: HourMarks {
implicitSize: 135 * (1.75 - 0.75 * hourMarksLoader.opacity) implicitSize: 135 * (1.75 - 0.75 * hourMarksLoader.opacity)
color: root.colOnBackground color: root.colOnBackground
@@ -146,7 +141,7 @@ Item {
FadeLoader { FadeLoader {
id: timeColumnLoader id: timeColumnLoader
anchors.centerIn: parent anchors.centerIn: parent
shown: Config.options.background.clock.cookie.timeIndicators shown: Config.options.background.widgets.clock.cookie.timeIndicators
scale: 1.4 - 0.4 * timeColumnLoader.shown scale: 1.4 - 0.4 * timeColumnLoader.shown
Behavior on scale { Behavior on scale {
animation: Appearance.animation.elementResize.numberAnimation.createObject(this) animation: Appearance.animation.elementResize.numberAnimation.createObject(this)
@@ -161,11 +156,11 @@ Item {
FadeLoader { FadeLoader {
anchors.fill: parent anchors.fill: parent
z: 1 z: 1
shown: Config.options.background.clock.cookie.minuteHandStyle !== "hide" shown: Config.options.background.widgets.clock.cookie.minuteHandStyle !== "hide"
sourceComponent: MinuteHand { sourceComponent: MinuteHand {
anchors.fill: parent anchors.fill: parent
clockMinute: root.clockMinute clockMinute: root.clockMinute
style: Config.options.background.clock.cookie.minuteHandStyle style: Config.options.background.widgets.clock.cookie.minuteHandStyle
color: root.colMinuteHand color: root.colMinuteHand
} }
} }
@@ -174,11 +169,11 @@ Item {
FadeLoader { FadeLoader {
anchors.fill: parent anchors.fill: parent
z: item?.style === "hollow" ? 0 : 2 z: item?.style === "hollow" ? 0 : 2
shown: Config.options.background.clock.cookie.hourHandStyle !== "hide" shown: Config.options.background.widgets.clock.cookie.hourHandStyle !== "hide"
sourceComponent: HourHand { sourceComponent: HourHand {
clockHour: root.clockHour clockHour: root.clockHour
clockMinute: root.clockMinute clockMinute: root.clockMinute
style: Config.options.background.clock.cookie.hourHandStyle style: Config.options.background.widgets.clock.cookie.hourHandStyle
color: root.colHourHand color: root.colHourHand
} }
} }
@@ -186,13 +181,13 @@ Item {
// Second hand // Second hand
FadeLoader { FadeLoader {
id: secondHandLoader id: secondHandLoader
z: (Config.options.background.clock.cookie.secondHandStyle === "line") ? 2 : 3 z: (Config.options.background.widgets.clock.cookie.secondHandStyle === "line") ? 2 : 3
shown: Config.options.time.secondPrecision && Config.options.background.clock.cookie.secondHandStyle !== "hide" shown: Config.options.time.secondPrecision && Config.options.background.widgets.clock.cookie.secondHandStyle !== "hide"
anchors.fill: parent anchors.fill: parent
sourceComponent: SecondHand { sourceComponent: SecondHand {
id: secondHand id: secondHand
clockSecond: root.clockSecond clockSecond: root.clockSecond
style: Config.options.background.clock.cookie.secondHandStyle style: Config.options.background.widgets.clock.cookie.secondHandStyle
color: root.colSecondHand color: root.colSecondHand
} }
} }
@@ -201,9 +196,9 @@ Item {
FadeLoader { FadeLoader {
z: 4 z: 4
anchors.centerIn: parent anchors.centerIn: parent
shown: Config.options.background.clock.cookie.minuteHandStyle !== "bold" shown: Config.options.background.widgets.clock.cookie.minuteHandStyle !== "bold"
sourceComponent: Rectangle { sourceComponent: Rectangle {
color: Config.options.background.clock.cookie.minuteHandStyle === "medium" ? root.colBackground : root.colMinuteHand color: Config.options.background.widgets.clock.cookie.minuteHandStyle === "medium" ? root.colBackground : root.colMinuteHand
implicitWidth: 6 implicitWidth: 6
implicitHeight: implicitWidth implicitHeight: implicitWidth
radius: width / 2 radius: width / 2
@@ -213,11 +208,11 @@ Item {
// Date // Date
FadeLoader { FadeLoader {
anchors.fill: parent anchors.fill: parent
shown: Config.options.background.clock.cookie.dateStyle !== "hide" shown: Config.options.background.widgets.clock.cookie.dateStyle !== "hide"
sourceComponent: DateIndicator { sourceComponent: DateIndicator {
color: root.colBackgroundInfo color: root.colBackgroundInfo
style: Config.options.background.clock.cookie.dateStyle style: Config.options.background.widgets.clock.cookie.dateStyle
} }
} }
} }
@@ -7,7 +7,7 @@ import Qt5Compat.GraphicalEffects
Item { Item {
id: root id: root
readonly property string quoteText: Config.options.background.quote readonly property string quoteText: Config.options.background.widgets.clock.quote.text
implicitWidth: quoteBox.implicitWidth implicitWidth: quoteBox.implicitWidth
implicitHeight: quoteBox.implicitHeight implicitHeight: quoteBox.implicitHeight
@@ -47,7 +47,7 @@ Item {
StyledText { StyledText {
id: quoteStyledText id: quoteStyledText
horizontalAlignment: Text.AlignLeft horizontalAlignment: Text.AlignLeft
text: Config.options.background.quote text: Config.options.background.widgets.clock.quote.text
color: Appearance.colors.colOnSecondaryContainer color: Appearance.colors.colOnSecondaryContainer
font { font {
family: Appearance.font.family.reading family: Appearance.font.family.reading
@@ -18,7 +18,7 @@ Item {
rotation: (360 / 60 * clockSecond) + 90 rotation: (360 / 60 * clockSecond) + 90
Behavior on rotation { Behavior on rotation {
enabled: Config.options.background.clock.cookie.constantlyRotate // Animating every second is expensive... enabled: Config.options.background.widgets.clock.cookie.constantlyRotate // Animating every second is expensive...
animation: RotationAnimation { animation: RotationAnimation {
direction: RotationAnimation.Clockwise direction: RotationAnimation.Clockwise
duration: 1000 // 1 second duration: 1000 // 1 second
@@ -8,10 +8,10 @@ import QtQuick
Column { Column {
id: root id: root
property list<string> clockNumbers: DateTime.time.split(/[: ]/) property list<string> clockNumbers: DateTime.time.split(/[: ]/)
property bool isEnabled: Config.options.background.clock.cookie.timeIndicators property bool isEnabled: Config.options.background.widgets.clock.cookie.timeIndicators
property color color: Appearance.colors.colOnSecondaryContainer property color color: Appearance.colors.colOnSecondaryContainer
property bool hourMarksEnabled: Config.options.background.clock.cookie.hourMarks property bool hourMarksEnabled: Config.options.background.widgets.clock.cookie.hourMarks
spacing: -16 spacing: -16
Repeater { Repeater {
@@ -14,7 +14,7 @@ Item {
// Rotating date // Rotating date
FadeLoader { FadeLoader {
anchors.fill: parent anchors.fill: parent
shown: Config.options.background.clock.cookie.dateStyle === "border" shown: Config.options.background.widgets.clock.cookie.dateStyle === "border"
sourceComponent: RotatingDate { sourceComponent: RotatingDate {
color: root.color color: root.color
} }
@@ -6,7 +6,7 @@ import QtQuick
Rectangle { Rectangle {
id: rect id: rect
readonly property string dialStyle: Config.options.background.clock.cookie.dialNumberStyle readonly property string dialStyle: Config.options.background.widgets.clock.cookie.dialNumberStyle
StyledText { StyledText {
anchors.centerIn: parent anchors.centerIn: parent
@@ -8,14 +8,14 @@ import QtQuick
Item { Item {
id: root id: root
property string style: Config.options.background.clock.cookie.dateStyle property string style: Config.options.background.widgets.clock.cookie.dateStyle
property color color: Appearance.colors.colOnSecondaryContainer property color color: Appearance.colors.colOnSecondaryContainer
property real angleStep: 12 * Math.PI / 180 property real angleStep: 12 * Math.PI / 180
property string dateText: Qt.locale().toString(DateTime.clock.date, "ddd dd") property string dateText: Qt.locale().toString(DateTime.clock.date, "ddd dd")
readonly property int clockSecond: DateTime.clock.seconds readonly property int clockSecond: DateTime.clock.seconds
readonly property string dialStyle: Config.options.background.clock.cookie.dialNumberStyle readonly property string dialStyle: Config.options.background.widgets.clock.cookie.dialNumberStyle
readonly property bool timeIndicators: Config.options.background.clock.cookie.timeIndicators readonly property bool timeIndicators: Config.options.background.widgets.clock.cookie.timeIndicators
property real radius: style === "border" ? 90 : 0 property real radius: style === "border" ? 90 : 0
Behavior on radius { Behavior on radius {
@@ -8,8 +8,8 @@ Item {
id: root id: root
property color color: Appearance.colors.colOnSecondaryContainer property color color: Appearance.colors.colOnSecondaryContainer
property string style: Config.options.background.clock.cookie.dialNumberStyle // "dots", "numbers", "full", "hide" property string style: Config.options.background.widgets.clock.cookie.dialNumberStyle // "dots", "numbers", "full", "hide"
property string dateStyle : Config.options.background.clock.cookie.dateStyle property string dateStyle : Config.options.background.widgets.clock.cookie.dateStyle
// 12 Dots // 12 Dots
FadeLoader { FadeLoader {
@@ -0,0 +1,58 @@
import QtQuick
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.modules.common.widgets.widgetCanvas
import qs.modules.background.widgets
AbstractBackgroundWidget {
id: root
configEntryName: "weather"
implicitHeight: backgroundShape.implicitHeight
implicitWidth: backgroundShape.implicitWidth
StyledDropShadow {
target: backgroundShape
}
MaterialShape {
id: backgroundShape
anchors.fill: parent
shape: MaterialShape.Shape.Pill
color: Appearance.colors.colPrimaryContainer
implicitSize: 200
StyledText {
font {
pixelSize: 80
family: Appearance.font.family.expressive
weight: Font.Medium
}
color: Appearance.colors.colPrimary
text: Weather.data?.temp.substring(0,Weather.data?.temp.length - 1) ?? "--°"
anchors {
right: parent.right
top: parent.top
rightMargin: 16
topMargin: 20
}
}
MaterialSymbol {
iconSize: 80
color: Appearance.colors.colOnPrimaryContainer
text: Icons.getWeatherIcon(Weather.data.wCode) ?? "cloud"
anchors {
left: parent.left
bottom: parent.bottom
leftMargin: 16
bottomMargin: 20
}
}
}
}
@@ -13,121 +13,60 @@ StyledPopup {
spacing: 4 spacing: 4
// Header // Header
Row { StyledPopupHeaderRow {
id: header icon: "battery_android_full"
spacing: 5 label: Translation.tr("Battery")
MaterialSymbol {
anchors.verticalCenter: parent.verticalCenter
fill: 0
font.weight: Font.Medium
text: "battery_android_full"
iconSize: Appearance.font.pixelSize.large
color: Appearance.colors.colOnSurfaceVariant
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: "Battery"
font {
weight: Font.Medium
pixelSize: Appearance.font.pixelSize.normal
}
color: Appearance.colors.colOnSurfaceVariant
}
} }
// This row is hidden when the battery is full. StyledPopupValueRow {
RowLayout { visible: {
spacing: 5
Layout.fillWidth: true
property bool rowVisible: {
let timeValue = Battery.isCharging ? Battery.timeToFull : Battery.timeToEmpty; let timeValue = Battery.isCharging ? Battery.timeToFull : Battery.timeToEmpty;
let power = Battery.energyRate; let power = Battery.energyRate;
return !(Battery.chargeState == 4 || timeValue <= 0 || power <= 0.01); return !(Battery.chargeState == 4 || timeValue <= 0 || power <= 0.01);
} }
visible: rowVisible icon: "schedule"
opacity: rowVisible ? 1 : 0 label: Battery.isCharging ? Translation.tr("Time to full:") : Translation.tr("Time to empty:")
Behavior on opacity { value: {
NumberAnimation { function formatTime(seconds) {
duration: 500 var h = Math.floor(seconds / 3600);
var m = Math.floor((seconds % 3600) / 60);
if (h > 0)
return `${h}h, ${m}m`;
else
return `${m}m`;
}
if (Battery.isCharging)
return formatTime(Battery.timeToFull);
else
return formatTime(Battery.timeToEmpty);
}
}
StyledPopupValueRow {
visible: !(Battery.chargeState != 4 && Battery.energyRate == 0)
icon: "bolt"
label: {
if (Battery.chargeState == 4) {
return Translation.tr("Fully charged");
} else if (Battery.chargeState == 1) {
return Translation.tr("Charging:");
} else {
return Translation.tr("Discharging:");
} }
} }
value: {
MaterialSymbol { if (Battery.chargeState == 4) {
text: "schedule" return "";
color: Appearance.colors.colOnSurfaceVariant } else {
iconSize: Appearance.font.pixelSize.large return `${Battery.energyRate.toFixed(2)}W`;
}
StyledText {
text: Battery.isCharging ? Translation.tr("Time to full:") : Translation.tr("Time to empty:")
color: Appearance.colors.colOnSurfaceVariant
}
StyledText {
Layout.fillWidth: true
horizontalAlignment: Text.AlignRight
color: Appearance.colors.colOnSurfaceVariant
text: {
function formatTime(seconds) {
var h = Math.floor(seconds / 3600);
var m = Math.floor((seconds % 3600) / 60);
if (h > 0)
return `${h}h, ${m}m`;
else
return `${m}m`;
}
if (Battery.isCharging)
return formatTime(Battery.timeToFull);
else
return formatTime(Battery.timeToEmpty);
} }
} }
} }
RowLayout { StyledPopupValueRow {
spacing: 5 icon: "heart_check"
Layout.fillWidth: true label: Translation.tr("Health:")
value: `${(Battery.health).toFixed(1)}%`
property bool rowVisible: !(Battery.chargeState != 4 && Battery.energyRate == 0)
visible: rowVisible
opacity: rowVisible ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: 500
}
}
MaterialSymbol {
text: "bolt"
color: Appearance.colors.colOnSurfaceVariant
iconSize: Appearance.font.pixelSize.large
}
StyledText {
text: {
if (Battery.chargeState == 4) {
return Translation.tr("Fully charged");
} else if (Battery.chargeState == 1) {
return Translation.tr("Charging:");
} else {
return Translation.tr("Discharging:");
}
}
color: Appearance.colors.colOnSurfaceVariant
}
StyledText {
Layout.fillWidth: true
horizontalAlignment: Text.AlignRight
color: Appearance.colors.colOnSurfaceVariant
text: {
if (Battery.chargeState == 4) {
return "";
} else {
return `${Battery.energyRate.toFixed(2)}W`;
}
}
}
} }
} }
} }
@@ -43,7 +43,7 @@ Item {
hoverEnabled: true hoverEnabled: true
acceptedButtons: Qt.NoButton acceptedButtons: Qt.NoButton
ClockWidgetTooltip { ClockWidgetPopup {
hoverTarget: mouseArea hoverTarget: mouseArea
} }
} }
@@ -0,0 +1,70 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Layouts
StyledPopup {
id: root
property string formattedDate: Qt.locale().toString(DateTime.clock.date, "dddd, MMMM dd, yyyy")
property string formattedTime: DateTime.time
property string formattedUptime: DateTime.uptime
property string todosSection: getUpcomingTodos()
function getUpcomingTodos() {
const unfinishedTodos = Todo.list.filter(function (item) {
return !item.done;
});
if (unfinishedTodos.length === 0) {
return Translation.tr("No pending tasks");
}
// Limit to first 5 todos to keep popup manageable
const limitedTodos = unfinishedTodos.slice(0, 5);
let todoText = limitedTodos.map(function (item, index) {
return ` ${index + 1}. ${item.content}`;
}).join('\n');
if (unfinishedTodos.length > 5) {
todoText += `\n ${Translation.tr("... and %1 more").arg(unfinishedTodos.length - 5)}`;
}
return todoText;
}
ColumnLayout {
id: columnLayout
anchors.centerIn: parent
spacing: 4
StyledPopupHeaderRow {
icon: "calendar_month"
label: root.formattedDate
}
StyledPopupValueRow {
icon: "timelapse"
label: Translation.tr("System uptime:")
value: root.formattedUptime
}
// Tasks
Column {
spacing: 0
Layout.fillWidth: true
StyledPopupValueRow {
icon: "checklist"
label: Translation.tr("To Do:")
value: ""
}
StyledText {
horizontalAlignment: Text.AlignLeft
wrapMode: Text.Wrap
color: Appearance.colors.colOnSurfaceVariant
text: root.todosSection
}
}
}
}
@@ -1,110 +0,0 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Layouts
StyledPopup {
id: root
property string formattedDate: Qt.locale().toString(DateTime.clock.date, "dddd, MMMM dd, yyyy")
property string formattedTime: DateTime.time
property string formattedUptime: DateTime.uptime
property string todosSection: getUpcomingTodos()
function getUpcomingTodos() {
const unfinishedTodos = Todo.list.filter(function (item) {
return !item.done;
});
if (unfinishedTodos.length === 0) {
return Translation.tr("No pending tasks");
}
// Limit to first 5 todos to keep popup manageable
const limitedTodos = unfinishedTodos.slice(0, 5);
let todoText = limitedTodos.map(function (item, index) {
return `${index + 1}. ${item.content}`;
}).join('\n');
if (unfinishedTodos.length > 5) {
todoText += `\n${Translation.tr("... and %1 more").arg(unfinishedTodos.length - 5)}`;
}
return todoText;
}
ColumnLayout {
id: columnLayout
anchors.centerIn: parent
spacing: 4
// Date + Time row
Row {
spacing: 5
MaterialSymbol {
anchors.verticalCenter: parent.verticalCenter
fill: 0
font.weight: Font.Medium
text: "calendar_month"
iconSize: Appearance.font.pixelSize.large
color: Appearance.colors.colOnSurfaceVariant
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
horizontalAlignment: Text.AlignLeft
color: Appearance.colors.colOnSurfaceVariant
text: `${root.formattedDate}`
font.weight: Font.Medium
}
}
// Uptime row
RowLayout {
spacing: 5
Layout.fillWidth: true
MaterialSymbol {
text: "timelapse"
color: Appearance.colors.colOnSurfaceVariant
font.pixelSize: Appearance.font.pixelSize.large
}
StyledText {
text: Translation.tr("System uptime:")
color: Appearance.colors.colOnSurfaceVariant
}
StyledText {
Layout.fillWidth: true
horizontalAlignment: Text.AlignRight
color: Appearance.colors.colOnSurfaceVariant
text: root.formattedUptime
}
}
// Tasks
Column {
spacing: 0
Layout.fillWidth: true
Row {
spacing: 4
MaterialSymbol {
anchors.verticalCenter: parent.verticalCenter
text: "checklist"
color: Appearance.colors.colOnSurfaceVariant
font.pixelSize: Appearance.font.pixelSize.large
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: Translation.tr("To Do:")
color: Appearance.colors.colOnSurfaceVariant
}
}
StyledText {
horizontalAlignment: Text.AlignLeft
wrapMode: Text.Wrap
color: Appearance.colors.colOnSurfaceVariant
text: root.todosSection
}
}
}
}
@@ -12,57 +12,6 @@ StyledPopup {
return (kb / (1024 * 1024)).toFixed(1) + " GB"; return (kb / (1024 * 1024)).toFixed(1) + " GB";
} }
component ResourceItem: RowLayout {
id: resourceItem
required property string icon
required property string label
required property string value
spacing: 4
MaterialSymbol {
text: resourceItem.icon
color: Appearance.colors.colOnSurfaceVariant
iconSize: Appearance.font.pixelSize.large
}
StyledText {
text: resourceItem.label
color: Appearance.colors.colOnSurfaceVariant
}
StyledText {
Layout.fillWidth: true
horizontalAlignment: Text.AlignRight
visible: resourceItem.value !== ""
color: Appearance.colors.colOnSurfaceVariant
text: resourceItem.value
}
}
component ResourceHeaderItem: Row {
id: headerItem
required property var icon
required property var label
spacing: 5
MaterialSymbol {
anchors.verticalCenter: parent.verticalCenter
fill: 0
font.weight: Font.Medium
text: headerItem.icon
iconSize: Appearance.font.pixelSize.large
color: Appearance.colors.colOnSurfaceVariant
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: headerItem.label
font {
weight: Font.Medium
pixelSize: Appearance.font.pixelSize.normal
}
color: Appearance.colors.colOnSurfaceVariant
}
}
Row { Row {
anchors.centerIn: parent anchors.centerIn: parent
spacing: 12 spacing: 12
@@ -71,23 +20,23 @@ StyledPopup {
anchors.top: parent.top anchors.top: parent.top
spacing: 8 spacing: 8
ResourceHeaderItem { StyledPopupHeaderRow {
icon: "memory" icon: "memory"
label: "RAM" label: "RAM"
} }
Column { Column {
spacing: 4 spacing: 4
ResourceItem { StyledPopupValueRow {
icon: "clock_loader_60" icon: "clock_loader_60"
label: Translation.tr("Used:") label: Translation.tr("Used:")
value: root.formatKB(ResourceUsage.memoryUsed) value: root.formatKB(ResourceUsage.memoryUsed)
} }
ResourceItem { StyledPopupValueRow {
icon: "check_circle" icon: "check_circle"
label: Translation.tr("Free:") label: Translation.tr("Free:")
value: root.formatKB(ResourceUsage.memoryFree) value: root.formatKB(ResourceUsage.memoryFree)
} }
ResourceItem { StyledPopupValueRow {
icon: "empty_dashboard" icon: "empty_dashboard"
label: Translation.tr("Total:") label: Translation.tr("Total:")
value: root.formatKB(ResourceUsage.memoryTotal) value: root.formatKB(ResourceUsage.memoryTotal)
@@ -100,23 +49,23 @@ StyledPopup {
anchors.top: parent.top anchors.top: parent.top
spacing: 8 spacing: 8
ResourceHeaderItem { StyledPopupHeaderRow {
icon: "swap_horiz" icon: "swap_horiz"
label: "Swap" label: "Swap"
} }
Column { Column {
spacing: 4 spacing: 4
ResourceItem { StyledPopupValueRow {
icon: "clock_loader_60" icon: "clock_loader_60"
label: Translation.tr("Used:") label: Translation.tr("Used:")
value: root.formatKB(ResourceUsage.swapUsed) value: root.formatKB(ResourceUsage.swapUsed)
} }
ResourceItem { StyledPopupValueRow {
icon: "check_circle" icon: "check_circle"
label: Translation.tr("Free:") label: Translation.tr("Free:")
value: root.formatKB(ResourceUsage.swapFree) value: root.formatKB(ResourceUsage.swapFree)
} }
ResourceItem { StyledPopupValueRow {
icon: "empty_dashboard" icon: "empty_dashboard"
label: Translation.tr("Total:") label: Translation.tr("Total:")
value: root.formatKB(ResourceUsage.swapTotal) value: root.formatKB(ResourceUsage.swapTotal)
@@ -128,16 +77,16 @@ StyledPopup {
anchors.top: parent.top anchors.top: parent.top
spacing: 8 spacing: 8
ResourceHeaderItem { StyledPopupHeaderRow {
icon: "planner_review" icon: "planner_review"
label: "CPU" label: "CPU"
} }
Column { Column {
spacing: 4 spacing: 4
ResourceItem { StyledPopupValueRow {
icon: "bolt" icon: "bolt"
label: Translation.tr("Load:") label: Translation.tr("Load:")
value: (ResourceUsage.cpuUsage > 0.8 ? Translation.tr("High") : ResourceUsage.cpuUsage > 0.4 ? Translation.tr("Medium") : Translation.tr("Low")) + ` (${Math.round(ResourceUsage.cpuUsage * 100)}%)` value: `${Math.round(ResourceUsage.cpuUsage * 100)}%`
} }
} }
} }
@@ -0,0 +1,30 @@
import QtQuick
import QtQuick.Layouts
import qs.modules.common
import qs.modules.common.widgets
Row {
id: root
required property var icon
required property var label
spacing: 5
MaterialSymbol {
anchors.verticalCenter: parent.verticalCenter
fill: 0
font.weight: Font.DemiBold
text: root.icon
iconSize: Appearance.font.pixelSize.large
color: Appearance.colors.colOnSurfaceVariant
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: root.label
font {
weight: Font.DemiBold
pixelSize: Appearance.font.pixelSize.normal
}
color: Appearance.colors.colOnSurfaceVariant
}
}
@@ -0,0 +1,29 @@
import QtQuick
import QtQuick.Layouts
import qs.modules.common
import qs.modules.common.widgets
RowLayout {
id: root
required property string icon
required property string label
required property string value
spacing: 4
MaterialSymbol {
text: root.icon
color: Appearance.colors.colOnSurfaceVariant
iconSize: Appearance.font.pixelSize.large
}
StyledText {
text: root.label
color: Appearance.colors.colOnSurfaceVariant
}
StyledText {
Layout.fillWidth: true
horizontalAlignment: Text.AlignRight
visible: root.value !== ""
color: Appearance.colors.colOnSurfaceVariant
text: root.value
}
}
@@ -104,7 +104,6 @@ Item {
id: overflowPopup id: overflowPopup
hoverTarget: trayOverflowButton hoverTarget: trayOverflowButton
active: root.trayOverflowOpen && root.unpinnedItems.length > 0 active: root.trayOverflowOpen && root.unpinnedItems.length > 0
popupBackgroundMargin: 300 // This should be plenty... makes sure tooltips don't get cutoff (easily)
GridLayout { GridLayout {
id: trayOverflowLayout id: trayOverflowLayout
@@ -25,7 +25,7 @@ Item {
visible: Config.options.bar.utilButtons.showScreenSnip visible: Config.options.bar.utilButtons.showScreenSnip
sourceComponent: CircleUtilButton { sourceComponent: CircleUtilButton {
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
onClicked: Hyprland.dispatch("global quickshell:regionScreenshot") onClicked: Quickshell.execDetached(["qs", "-p", Quickshell.shellPath(""), "ipc", "call", "region", "screenshot"]);
MaterialSymbol { MaterialSymbol {
horizontalAlignment: Qt.AlignHCenter horizontalAlignment: Qt.AlignHCenter
fill: 1 fill: 1
@@ -30,7 +30,7 @@ MouseArea {
MaterialSymbol { MaterialSymbol {
fill: 0 fill: 0
text: WeatherIcons.codeToName[Weather.data.wCode] ?? "cloud" text: Icons.getWeatherIcon(Weather.data.wCode) ?? "cloud"
iconSize: Appearance.font.pixelSize.large iconSize: Appearance.font.pixelSize.large
color: Appearance.colors.colOnLayer1 color: Appearance.colors.colOnLayer1
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
@@ -1,59 +0,0 @@
pragma Singleton
import Quickshell
Singleton {
// credits: calestia
// this snippet is taken from
// https://github.com/caelestia-dots/shell
readonly property var codeToName: ({
"113": "clear_day",
"116": "partly_cloudy_day",
"119": "cloud",
"122": "cloud",
"143": "foggy",
"176": "rainy",
"179": "rainy",
"182": "rainy",
"185": "rainy",
"200": "thunderstorm",
"227": "cloudy_snowing",
"230": "snowing_heavy",
"248": "foggy",
"260": "foggy",
"263": "rainy",
"266": "rainy",
"281": "rainy",
"284": "rainy",
"293": "rainy",
"296": "rainy",
"299": "rainy",
"302": "weather_hail",
"305": "rainy",
"308": "weather_hail",
"311": "rainy",
"314": "rainy",
"317": "rainy",
"320": "cloudy_snowing",
"323": "cloudy_snowing",
"326": "cloudy_snowing",
"329": "snowing_heavy",
"332": "snowing_heavy",
"335": "snowing",
"338": "snowing_heavy",
"350": "rainy",
"353": "rainy",
"356": "rainy",
"359": "weather_hail",
"362": "rainy",
"365": "rainy",
"368": "cloudy_snowing",
"371": "snowing",
"374": "rainy",
"377": "rainy",
"386": "thunderstorm",
"389": "thunderstorm",
"392": "thunderstorm",
"395": "snowing"
})
}
@@ -145,36 +145,44 @@ Singleton {
} }
property JsonObject background: JsonObject { property JsonObject background: JsonObject {
property JsonObject clock: JsonObject { property JsonObject widgets: JsonObject {
property bool fixedPosition: false property JsonObject clock: JsonObject {
property real x: -500 property bool enable: true
property real y: -500 property string placementStrategy: "leastBusy" // "free", "leastBusy", "mostBusy"
property bool show: true property real x: 100
property string style: "cookie" // Options: "cookie", "digital" property real y: 100
property real scale: 1 property string style: "cookie" // Options: "cookie", "digital"
property JsonObject cookie: JsonObject { property JsonObject cookie: JsonObject {
property bool aiStyling: false property bool aiStyling: false
property int sides: 14 property int sides: 14
property string dialNumberStyle: "full" // Options: "dots" , "numbers", "full" , "none" property string dialNumberStyle: "full" // Options: "dots" , "numbers", "full" , "none"
property string hourHandStyle: "fill" // Options: "classic", "fill", "hollow", "hide" property string hourHandStyle: "fill" // Options: "classic", "fill", "hollow", "hide"
property string minuteHandStyle: "medium" // Options "classic", "thin", "medium", "bold", "hide" property string minuteHandStyle: "medium" // Options "classic", "thin", "medium", "bold", "hide"
property string secondHandStyle: "dot" // Options: "dot", "line", "classic", "hide" property string secondHandStyle: "dot" // Options: "dot", "line", "classic", "hide"
property string dateStyle: "bubble" // Options: "border", "rect", "bubble" , "hide" property string dateStyle: "bubble" // Options: "border", "rect", "bubble" , "hide"
property bool timeIndicators: true property bool timeIndicators: true
property bool hourMarks: false property bool hourMarks: false
property bool dateInClock: true property bool dateInClock: true
property bool constantlyRotate: false property bool constantlyRotate: false
property bool useSineCookie: false property bool useSineCookie: false
}
property JsonObject digital: JsonObject {
property bool animateChange: true
}
property JsonObject quote: JsonObject {
property bool enable: false
property string text: ""
}
} }
property JsonObject digital: JsonObject { property JsonObject weather: JsonObject {
property bool animateChange: true property bool enable: false
property string placementStrategy: "free" // "free", "leastBusy", "mostBusy"
property real x: 400
property real y: 100
} }
} }
property string wallpaperPath: "" property string wallpaperPath: ""
property string thumbnailPath: "" property string thumbnailPath: ""
property string quote: ""
property bool showQuote: false
property bool hideWhenFullscreen: true property bool hideWhenFullscreen: true
property JsonObject parallax: JsonObject { property JsonObject parallax: JsonObject {
property bool vertical: false property bool vertical: false
@@ -182,7 +190,7 @@ Singleton {
property bool enableWorkspace: true property bool enableWorkspace: true
property real workspaceZoom: 1.07 // Relative to your screen, not wallpaper size property real workspaceZoom: 1.07 // Relative to your screen, not wallpaper size
property bool enableSidebar: true property bool enableSidebar: true
property real clockFactor: 1.2 property real widgetsFactor: 1.2
} }
} }
@@ -318,7 +326,7 @@ Singleton {
property bool useHyprlock: false property bool useHyprlock: false
property bool launchOnStartup: false property bool launchOnStartup: false
property JsonObject blur: JsonObject { property JsonObject blur: JsonObject {
property bool enable: false property bool enable: true
property real radius: 100 property real radius: 100
property real extraZoom: 1.1 property real extraZoom: 1.1
} }
@@ -353,6 +361,11 @@ Singleton {
property bool pinnedOnStartup: false property bool pinnedOnStartup: false
} }
property JsonObject overlay: JsonObject {
property bool openingZoomAnimation: true
property bool darkenScreen: true
}
property JsonObject overview: JsonObject { property JsonObject overview: JsonObject {
property bool enable: true property bool enable: true
property real scale: 0.18 // Relative to screen size property real scale: 0.18 // Relative to screen size
@@ -369,6 +382,7 @@ Singleton {
property bool showLabel: false property bool showLabel: false
property real opacity: 0.3 property real opacity: 0.3
property real contentRegionOpacity: 0.8 property real contentRegionOpacity: 0.8
property int selectionPadding: 5
} }
property JsonObject rect: JsonObject { property JsonObject rect: JsonObject {
property bool showAimLines: true property bool showAimLines: true
@@ -461,6 +475,14 @@ Singleton {
} }
} }
property JsonObject screenRecord: JsonObject {
property string savePath: Directories.videos.replace("file://","") // strip "file://"
}
property JsonObject screenSnip: JsonObject {
property string savePath: "" // only copy to clipboard when empty
}
property JsonObject sounds: JsonObject { property JsonObject sounds: JsonObject {
property bool battery: false property bool battery: false
property bool pomodoro: false property bool pomodoro: false
@@ -20,4 +20,63 @@ Singleton {
return "keyboard"; return "keyboard";
return "bluetooth"; return "bluetooth";
} }
readonly property var weatherIconMap: ({
"113": "clear_day",
"116": "partly_cloudy_day",
"119": "cloud",
"122": "cloud",
"143": "foggy",
"176": "rainy",
"179": "rainy",
"182": "rainy",
"185": "rainy",
"200": "thunderstorm",
"227": "cloudy_snowing",
"230": "snowing_heavy",
"248": "foggy",
"260": "foggy",
"263": "rainy",
"266": "rainy",
"281": "rainy",
"284": "rainy",
"293": "rainy",
"296": "rainy",
"299": "rainy",
"302": "weather_hail",
"305": "rainy",
"308": "weather_hail",
"311": "rainy",
"314": "rainy",
"317": "rainy",
"320": "cloudy_snowing",
"323": "cloudy_snowing",
"326": "cloudy_snowing",
"329": "snowing_heavy",
"332": "snowing_heavy",
"335": "snowing",
"338": "snowing_heavy",
"350": "rainy",
"353": "rainy",
"356": "rainy",
"359": "weather_hail",
"362": "rainy",
"365": "rainy",
"368": "cloudy_snowing",
"371": "snowing",
"374": "rainy",
"377": "rainy",
"386": "thunderstorm",
"389": "thunderstorm",
"392": "thunderstorm",
"395": "snowing"
})
function getWeatherIcon(code) {
const key = String(code)
if (weatherIconMap.hasOwnProperty(key)) {
return weatherIconMap[key]
}
}
} }
@@ -79,6 +79,28 @@ Singleton {
property bool inhibit: false property bool inhibit: false
} }
property JsonObject overlay: JsonObject {
property list<string> open: ["crosshair"]
property JsonObject crosshair: JsonObject {
property bool pinned: false
property bool clickthrough: true
property real x: 835
property real y: 490
}
property JsonObject recorder: JsonObject {
property bool pinned: false
property bool clickthrough: false
property real x: 100
property real y: 130
}
property JsonObject volumeMixer: JsonObject {
property bool pinned: false
property bool clickthrough: false
property real x: 100
property real y: 320
}
}
property JsonObject timer: JsonObject { property JsonObject timer: JsonObject {
property JsonObject pomodoro: JsonObject { property JsonObject pomodoro: JsonObject {
property bool running: false property bool running: false
@@ -0,0 +1,13 @@
import QtQuick
import Qt5Compat.GraphicalEffects
import qs.modules.common
DropShadow {
required property var target
source: target
anchors.fill: source
radius: 8
samples: radius * 2 + 1
color: Appearance.colors.colShadow
transparentBorder: true
}
@@ -0,0 +1,13 @@
import QtQuick
import Quickshell
import qs.modules.common
/*
* Abstract widgets for an overlay. Doesn't contain any visuals.
*/
AbstractWidget {
id: root
property bool pinned: false // Whether to stay visible when the overlay is dismissed
property bool clickthrough: true // When pinned, whether to allow clicks go through
}
@@ -0,0 +1,26 @@
import QtQuick
import Quickshell
import qs.modules.common
/*
* Widget to be placed on a WidgetCanvas
*/
MouseArea {
id: root
property bool draggable: true
drag.target: draggable ? root : undefined
cursorShape: (draggable && containsPress) ? Qt.ClosedHandCursor : draggable ? Qt.OpenHandCursor : Qt.ArrowCursor
function center() {
root.x = (root.parent.width - root.width) / 2
root.y = (root.parent.height - root.height) / 2
}
Behavior on x {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
Behavior on y {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
}
@@ -0,0 +1,7 @@
import QtQuick
MouseArea {
id: root
// uh this is stupid turns out we don't need anything here
}
@@ -1,57 +0,0 @@
import qs
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Hyprland
Scope {
id: root
Loader {
id: crosshairLoader
active: GlobalStates.crosshairOpen
sourceComponent: PanelWindow {
id: crosshairWindow
exclusionMode: ExclusionMode.Ignore
WlrLayershell.namespace: "quickshell:crosshair"
WlrLayershell.layer: WlrLayer.Overlay
visible: true
color: "transparent"
mask: Region { // Crosshair should not block mouse input
item: null
}
implicitWidth: crosshairContent.implicitWidth
implicitHeight: crosshairContent.implicitHeight
CrosshairContent {
id: crosshairContent
anchors.centerIn: parent
}
}
}
IpcHandler {
target: "crosshair"
function toggle(): void {
GlobalStates.crosshairOpen = !GlobalStates.crosshairOpen;
}
}
GlobalShortcut {
name: "crosshairToggle"
description: "Toggles crosshair on press"
onPressed: {
GlobalStates.crosshairOpen = !GlobalStates.crosshairOpen;
}
}
}
@@ -128,11 +128,19 @@ Scope {
} }
} }
function lock() {
if (Config.options.lock.useHyprlock) {
Quickshell.execDetached(["bash", "-c", "pidof hyprlock || hyprlock"]);
return;
}
GlobalStates.screenLocked = true;
}
IpcHandler { IpcHandler {
target: "lock" target: "lock"
function activate(): void { function activate(): void {
GlobalStates.screenLocked = true; root.lock();
} }
function focus(): void { function focus(): void {
lockContext.shouldReFocus(); lockContext.shouldReFocus();
@@ -144,11 +152,7 @@ Scope {
description: "Locks the screen" description: "Locks the screen"
onPressed: { onPressed: {
if (Config.options.lock.useHyprlock) { root.lock()
Quickshell.execDetached(["bash", "-c", "pidof hyprlock || hyprlock"]);
return;
}
GlobalStates.screenLocked = true;
} }
} }
@@ -165,7 +169,7 @@ Scope {
function initIfReady() { function initIfReady() {
if (!Config.ready || !Persistent.ready) return; if (!Config.ready || !Persistent.ready) return;
if (Config.options.lock.launchOnStartup && Persistent.isNewHyprlandInstance) { if (Config.options.lock.launchOnStartup && Persistent.isNewHyprlandInstance) {
Hyprland.dispatch("global quickshell:lock") root.lock();
} else { } else {
KeyringStorage.fetchKeyringData(); KeyringStorage.fetchKeyringData();
} }
@@ -87,7 +87,7 @@ Scope {
} }
onExited: (exitCode, exitStatus) => { onExited: (exitCode, exitStatus) => {
if (exitCode !== 0) { if (exitCode !== 0) {
console.warn("fprintd-list command exited with error:", exitCode, exitStatus); // console.warn("[LockContext] fprintd-list command exited with error:", exitCode, exitStatus);
root.fingerprintsConfigured = false; root.fingerprintsConfigured = false;
} }
} }
@@ -0,0 +1,69 @@
import qs
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Hyprland
Scope {
id: root
property Component regionComponent: Component {
Region {}
}
Loader {
id: overlayLoader
active: GlobalStates.overlayOpen || OverlayContext.hasPinnedWidgets
sourceComponent: PanelWindow {
id: overlayWindow
exclusionMode: ExclusionMode.Ignore
WlrLayershell.namespace: "quickshell:overlay"
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
visible: true
color: "transparent"
mask: Region {
item: GlobalStates.overlayOpen ? overlayContent : null
regions: OverlayContext.clickableWidgets.map((widget) => regionComponent.createObject(this, {
item: widget
}));
}
anchors {
top: true
bottom: true
left: true
right: true
}
OverlayContent {
id: overlayContent
anchors.fill: parent
}
}
}
IpcHandler {
target: "overlay"
function toggle(): void {
GlobalStates.overlayOpen = !GlobalStates.overlayOpen;
}
}
GlobalShortcut {
name: "overlayToggle"
description: "Toggles overlay on press"
onPressed: {
GlobalStates.overlayOpen = !GlobalStates.overlayOpen;
}
}
}
@@ -0,0 +1,67 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.widgets.widgetCanvas
import qs.modules.overlay.crosshair
Item {
id: root
readonly property bool usePasswordChars: !PolkitService.flow?.responseVisible ?? true
Keys.onPressed: (event) => { // Esc to close
if (event.key === Qt.Key_Escape) {
GlobalStates.overlayOpen = false;
}
}
property real initScale: Config.options.overlay.openingZoomAnimation ? 1.08 : 1.000001
scale: initScale
Component.onCompleted: {
scale = 1
}
Behavior on scale {
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
}
Rectangle {
id: bg
anchors.fill: parent
color: Appearance.colors.colScrim
visible: Config.options.overlay.darkenScreen && opacity > 0
opacity: (GlobalStates.overlayOpen && root.scale !== initScale) ? 1 : 0
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
}
WidgetCanvas {
anchors.fill: parent
onClicked: GlobalStates.overlayOpen = false
OverlayTaskbar {
anchors {
horizontalCenter: parent.horizontalCenter
top: parent.top
topMargin: 50
}
}
Repeater {
model: ScriptModel {
values: Persistent.states.overlay.open.map(identifier => {
return OverlayContext.availableWidgets.find(w => w.identifier === identifier);
})
objectProp: "identifier"
}
delegate: OverlayWidgetDelegateChooser {
}
}
}
}
@@ -0,0 +1,38 @@
pragma Singleton
pragma ComponentBehavior: Bound
import Quickshell
Singleton {
id: root
readonly property list<var> availableWidgets: [
{ identifier: "crosshair", materialSymbol: "point_scan" },
{ identifier: "volumeMixer", materialSymbol: "volume_up" },
{ identifier: "recorder", materialSymbol: "screen_record" },
]
readonly property bool hasPinnedWidgets: root.pinnedWidgetIdentifiers.length > 0
property list<string> pinnedWidgetIdentifiers: []
property list<var> clickableWidgets: []
function pin(identifier: string, pin = true) {
if (pin) {
if (!root.pinnedWidgetIdentifiers.includes(identifier)) {
root.pinnedWidgetIdentifiers.push(identifier)
}
} else {
root.pinnedWidgetIdentifiers = root.pinnedWidgetIdentifiers.filter(id => id !== identifier)
}
}
function registerClickableWidget(widget: var, clickable = true) {
if (clickable) {
if (!root.clickableWidgets.includes(widget)) {
root.clickableWidgets.push(widget)
}
} else {
root.clickableWidgets = root.clickableWidgets.filter(w => w !== widget)
}
}
}
@@ -0,0 +1,113 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.modules.common.widgets.widgetCanvas
Rectangle {
id: root
property real padding: 8
opacity: GlobalStates.overlayOpen ? 1 : 0
implicitWidth: contentRow.implicitWidth + (padding * 2)
implicitHeight: contentRow.implicitHeight + (padding * 2)
color: Appearance.m3colors.m3surfaceContainer
radius: Appearance.rounding.large
border.color: Appearance.colors.colOutlineVariant
border.width: 1
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
RowLayout {
id: contentRow
anchors {
fill: parent
margins: root.padding
}
spacing: 6
Row {
spacing: 4
Repeater {
model: ScriptModel {
values: OverlayContext.availableWidgets
}
delegate: WidgetButton {
required property var modelData
identifier: modelData.identifier
materialSymbol: modelData.materialSymbol
}
}
}
Separator {}
TimeWidget {}
}
component Separator: Rectangle {
implicitWidth: 1
color: Appearance.colors.colOutlineVariant
Layout.fillHeight: true
Layout.topMargin: 10
Layout.bottomMargin: 10
}
component TimeWidget: StyledText {
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: 8
Layout.rightMargin: 6
text: DateTime.time
font {
family: Appearance.font.family.numbers
variableAxes: Appearance.font.variableAxes.numbers
pixelSize: 22
}
}
component WidgetButton: RippleButton {
id: widgetButton
required property string identifier
required property string materialSymbol
Layout.alignment: Qt.AlignVCenter
toggled: Persistent.states.overlay.open.includes(identifier)
onClicked: {
if (widgetButton.toggled) {
Persistent.states.overlay.open = Persistent.states.overlay.open.filter(type => type !== identifier);
} else {
Persistent.states.overlay.open.push(identifier);
}
}
implicitWidth: implicitHeight
colBackgroundToggled: Appearance.colors.colSecondaryContainer
colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover
colRippleToggled: Appearance.colors.colSecondaryContainerActive
buttonRadius: root.radius - (root.height - height) / 2
contentItem: Item {
anchors.centerIn: parent
implicitWidth: 32
implicitHeight: 32
MaterialSymbol {
id: iconWidget
anchors.centerIn: parent
iconSize: 24
text: widgetButton.materialSymbol
color: widgetButton.toggled ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnSurfaceVariant
}
}
}
}
@@ -0,0 +1,20 @@
pragma ComponentBehavior: Bound
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Bluetooth
import qs.modules.overlay.crosshair
import qs.modules.overlay.volumeMixer
import qs.modules.overlay.recorder
DelegateChooser {
id: root
role: "identifier"
DelegateChoice { roleValue: "crosshair"; Crosshair {} }
DelegateChoice { roleValue: "volumeMixer"; VolumeMixer {} }
DelegateChoice { roleValue: "recorder"; Recorder {} }
}
@@ -0,0 +1,230 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import Qt5Compat.GraphicalEffects
import qs
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.modules.common.widgets.widgetCanvas
/*
* To make an overlay widget:
* 1. Create a modules/overlay/<yourWidget>/<YourWidget>.qml, using this as the base class and declare your widget content as contentItem
* 2. Add an entry to OverlayContext.availableWidgets with identifier=<yourWidgetIdentifier>
* 3. Add an entry in Persistent.states.overlay.<yourWidgetIdentifier> with x, y, pinned, clickthrough properties set to reasonable defaults
* 4. Add an entry in OverlayWidgetDelegateChooser with roleValue=<yourWidgetIdentifier> and Declare your widget in there
* Use existing entries as reference.
*/
AbstractOverlayWidget {
id: root
required property Item contentItem
required property var modelData
readonly property string identifier: modelData.identifier
readonly property string materialSymbol: modelData.materialSymbol ?? "widgets"
property string title: identifier.replace(/([A-Z])/g, " $1").replace(/^./, function(str){ return str.toUpperCase(); })
property var persistentStateEntry: Persistent.states.overlay[identifier]
property real radius: Appearance.rounding.windowRounding
property real minWidth: 250
draggable: GlobalStates.overlayOpen
x: Math.round(persistentStateEntry.x) // Round or it'll be blurry
y: Math.round(persistentStateEntry.y) // Round or it'll be blurry
pinned: persistentStateEntry.pinned
clickthrough: persistentStateEntry.clickthrough
drag {
minimumX: 0
minimumY: 0
maximumX: root.parent.width - root.width
maximumY: root.parent.height - root.height
}
// Guarded states & registration funcs
property bool open: Persistent.states.overlay.open
property bool actuallyPinned: pinned && open
property bool actuallyClickable: !clickthrough && actuallyPinned && open
onActuallyPinnedChanged: reportPinnedState();
onActuallyClickableChanged: reportClickableState();
function reportPinnedState() {
OverlayContext.pin(identifier, actuallyPinned);
}
function reportClickableState() {
OverlayContext.registerClickableWidget(contentItem, actuallyClickable);
}
// Self-registeration with OverlayContext
Component.onCompleted: {
reportPinnedState();
reportClickableState();
}
// Hooks
onReleased: savePosition();
function close() {
Persistent.states.overlay.open = Persistent.states.overlay.open.filter(type => type !== root.identifier);
}
function togglePinned() {
persistentStateEntry.pinned = !persistentStateEntry.pinned;
}
function toggleClickthrough() {
persistentStateEntry.clickthrough = !persistentStateEntry.clickthrough;
}
function savePosition(xPos = root.x, yPos = root.y) {
persistentStateEntry.x = xPos;
persistentStateEntry.y = yPos;
}
function center() {
const targetX = (root.parent.width - contentColumn.width) / 2
const targetY = (root.parent.height - contentItem.height) / 2 - titleBar.implicitHeight
root.x = targetX
root.y = targetY
root.savePosition(targetX, targetY)
}
visible: GlobalStates.overlayOpen || actuallyPinned
implicitWidth: Math.max(contentColumn.implicitWidth, minWidth)
implicitHeight: contentColumn.implicitHeight
Rectangle {
id: border
anchors.fill: parent
color: "transparent"
radius: root.radius
border.color: ColorUtils.transparentize(Appearance.colors.colOutlineVariant, GlobalStates.overlayOpen ? 0 : 1)
border.width: 1
layer.enabled: GlobalStates.overlayOpen
layer.effect: OpacityMask {
maskSource: Rectangle {
width: border.width
height: border.height
radius: root.radius
}
}
Column {
id: contentColumn
z: -1
anchors.fill: parent
// Title bar
Rectangle {
id: titleBar
opacity: GlobalStates.overlayOpen ? 1 : 0
anchors {
left: parent.left
right: parent.right
}
property real padding: 2
implicitWidth: titleBarRow.implicitWidth + padding * 2
implicitHeight: titleBarRow.implicitHeight + padding * 2
color: Appearance.m3colors.m3surfaceContainer
border.color: Appearance.colors.colOutlineVariant
border.width: 1
RowLayout {
id: titleBarRow
anchors {
fill: parent
margins: titleBar.padding
leftMargin: titleBar.padding + 8
}
spacing: 0
MaterialSymbol {
text: root.materialSymbol
iconSize: 20
Layout.alignment: Qt.AlignVCenter
Layout.rightMargin: 4
}
StyledText {
text: root.title
Layout.fillWidth: true
elide: Text.ElideRight
}
TitlebarButton {
materialSymbol: "recenter"
onClicked: root.center()
StyledToolTip {
text: "Center"
}
}
TitlebarButton {
materialSymbol: "mouse"
toggled: !root.clickthrough
onClicked: root.toggleClickthrough()
StyledToolTip {
text: "Clickable when pinned"
}
}
TitlebarButton {
materialSymbol: "keep"
toggled: root.pinned
onClicked: root.togglePinned()
StyledToolTip {
text: "Pin"
}
}
TitlebarButton {
materialSymbol: "close"
onClicked: root.close()
StyledToolTip {
text: "Close"
}
}
}
}
// Content
Item {
id: contentContainer
anchors.horizontalCenter: parent.horizontalCenter
implicitWidth: root.contentItem.implicitWidth
implicitHeight: root.contentItem.implicitHeight
children: [root.contentItem]
}
}
}
component TitlebarButton: RippleButton {
id: titlebarButton
required property string materialSymbol
buttonRadius: height / 2
implicitHeight: contentItem.implicitHeight
implicitWidth: implicitHeight
padding: 0
colBackgroundToggled: Appearance.colors.colSecondaryContainer
colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover
colRippleToggled: Appearance.colors.colSecondaryContainerActive
contentItem: Item {
anchors.centerIn: parent
implicitWidth: 30
implicitHeight: 30
MaterialSymbol {
id: iconWidget
anchors.centerIn: parent
iconSize: 20
text: titlebarButton.materialSymbol
fill: titlebarButton.toggled
color: titlebarButton.toggled ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnSurface
}
}
}
}
@@ -0,0 +1,9 @@
import QtQuick
import Quickshell
import qs.modules.common
import qs.modules.overlay
StyledOverlayWidget {
id: root
contentItem: CrosshairContent {}
}
@@ -0,0 +1,122 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Hyprland
import qs
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.overlay
StyledOverlayWidget {
id: root
contentItem: Rectangle {
id: contentItem
anchors.centerIn: parent
color: Appearance.m3colors.m3surfaceContainer
property real padding: 8
implicitHeight: contentColumn.implicitHeight + padding * 2
implicitWidth: 350
ColumnLayout {
id: contentColumn
anchors {
fill: parent
margins: parent.padding
}
spacing: 10
Row {
Layout.alignment: Qt.AlignHCenter
spacing: 10
BigRecorderButton {
materialSymbol: "screenshot_region"
name: "Screenshot region"
onClicked: {
GlobalStates.overlayOpen = false;
Quickshell.execDetached(["qs", "-p", Quickshell.shellPath(""), "ipc", "call", "region", "screenshot"]);
}
}
BigRecorderButton {
materialSymbol: "photo_camera"
name: "Screenshot"
onClicked: {
GlobalStates.overlayOpen = false;
Quickshell.execDetached(["bash", "-c", "grim - | wl-copy"]);
}
}
BigRecorderButton {
materialSymbol: "screen_record"
name: "Record region"
onClicked: {
GlobalStates.overlayOpen = false;
Quickshell.execDetached(["qs", "-p", Quickshell.shellPath(""), "ipc", "call", "region", "recordWithSound"]);
}
}
BigRecorderButton {
materialSymbol: "capture"
name: "Record screen"
onClicked: {
GlobalStates.overlayOpen = false;
Quickshell.execDetached([Directories.recordScriptPath, "--fullscreen", "--sound"]);
}
}
}
RippleButton {
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: false
buttonRadius: height / 2
colBackground: Appearance.colors.colLayer3
colBackgroundHover: Appearance.colors.colLayer3Hover
colRipple: Appearance.colors.colLayer3Active
onClicked: {
GlobalStates.overlayOpen = false;
Qt.openUrlExternally(Directories.videos);
}
contentItem: Row {
anchors.centerIn: parent
spacing: 6
MaterialSymbol {
anchors.verticalCenter: parent.verticalCenter
text: "animated_images"
iconSize: 20
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: qsTr("Open recordings folder")
}
}
}
}
}
component BigRecorderButton: RippleButton {
id: bigButton
required property string materialSymbol
required property string name
implicitHeight: 66
implicitWidth: 66
buttonRadius: height / 2
colBackground: Appearance.colors.colLayer3
colBackgroundHover: Appearance.colors.colLayer3Hover
colRipple: Appearance.colors.colLayer3Active
contentItem: MaterialSymbol {
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
text: bigButton.materialSymbol
iconSize: 28
}
StyledToolTip {
text: bigButton.name
}
}
}
@@ -0,0 +1,24 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.modules.common
import qs.modules.overlay
import qs.modules.sidebarRight.volumeMixer
StyledOverlayWidget {
id: root
contentItem: Rectangle {
anchors.centerIn: parent
color: Appearance.m3colors.m3surfaceContainer
property real padding: 16
implicitHeight: 600
implicitWidth: 350
VolumeDialogContent {
anchors.fill: parent
anchors.margins: parent.padding
isSink: true
}
}
}
@@ -99,7 +99,7 @@ RowLayout {
Layout.bottomMargin: 4 Layout.bottomMargin: 4
onClicked: { onClicked: {
GlobalStates.overviewOpen = false; GlobalStates.overviewOpen = false;
Hyprland.dispatch("global quickshell:regionSearch") Quickshell.execDetached(["qs", "-p", Quickshell.shellPath(""), "ipc", "call", "region", "search"]);
} }
text: "image_search" text: "image_search"
StyledToolTip { StyledToolTip {
@@ -33,6 +33,10 @@ PanelWindow {
property var selectionMode: RegionSelection.SelectionMode.RectCorners property var selectionMode: RegionSelection.SelectionMode.RectCorners
signal dismiss() signal dismiss()
property string saveScreenshotDir: Config.options.screenSnip.savePath !== ""
? Config.options.screenSnip.savePath
: ""
property string screenshotDir: Directories.screenshotTemp property string screenshotDir: Directories.screenshotTemp
property string imageSearchEngineBaseUrl: Config.options.search.imageSearch.imageSearchEngineBaseUrl property string imageSearchEngineBaseUrl: Config.options.search.imageSearch.imageSearchEngineBaseUrl
property string fileUploadApiEndpoint: "https://uguu.se/upload" property string fileUploadApiEndpoint: "https://uguu.se/upload"
@@ -121,10 +125,11 @@ PanelWindow {
return (root.targetedRegionX >= 0 && root.targetedRegionY >= 0) return (root.targetedRegionX >= 0 && root.targetedRegionY >= 0)
} }
function setRegionToTargeted() { function setRegionToTargeted() {
root.regionX = root.targetedRegionX; const padding = Config.options.regionSelector.targetRegions.selectionPadding; // Make borders not cut off n stuff
root.regionY = root.targetedRegionY; root.regionX = root.targetedRegionX - padding;
root.regionWidth = root.targetedRegionWidth; root.regionY = root.targetedRegionY - padding;
root.regionHeight = root.targetedRegionHeight; root.regionWidth = root.targetedRegionWidth + padding * 2;
root.regionHeight = root.targetedRegionHeight + padding * 2;
} }
function updateTargetedRegion(x, y) { function updateTargetedRegion(x, y) {
@@ -258,7 +263,23 @@ PanelWindow {
} }
switch (root.action) { switch (root.action) {
case RegionSelection.SnipAction.Copy: case RegionSelection.SnipAction.Copy:
snipProc.command = ["bash", "-c", `${cropToStdout} | wl-copy && ${cleanup}`] if (saveScreenshotDir === "") {
// not saving the screenshot, just copy to clipboard
snipProc.command = ["bash", "-c", `${cropToStdout} | wl-copy && ${cleanup}`]
break;
}
const savePathBase = root.saveScreenshotDir
snipProc.command = [
"bash", "-c",
`mkdir -p '${StringUtils.shellSingleQuoteEscape(savePathBase)}' && \
saveFileName="screenshot-$(date '+%Y-%m-%d_%H.%M.%S').png" && \
savePath="${savePathBase}/$saveFileName" && \
${cropToStdout} | tee >(wl-copy) > "$savePath" && \
${cleanup}`
]
break; break;
case RegionSelection.SnipAction.Edit: case RegionSelection.SnipAction.Edit:
snipProc.command = ["bash", "-c", `${cropToStdout} | swappy -f - && ${cleanup}`] snipProc.command = ["bash", "-c", `${cropToStdout} | swappy -f - && ${cleanup}`]
@@ -0,0 +1,471 @@
import QtQuick
import QtQuick.Layouts
import qs.services
import qs.modules.common
import qs.modules.common.widgets
ContentPage {
forceWidth: true
ContentSection {
icon: "sync_alt"
title: Translation.tr("Parallax")
ConfigSwitch {
buttonIcon: "unfold_more_double"
text: Translation.tr("Vertical")
checked: Config.options.background.parallax.vertical
onCheckedChanged: {
Config.options.background.parallax.vertical = checked;
}
}
ConfigRow {
uniform: true
ConfigSwitch {
buttonIcon: "counter_1"
text: Translation.tr("Depends on workspace")
checked: Config.options.background.parallax.enableWorkspace
onCheckedChanged: {
Config.options.background.parallax.enableWorkspace = checked;
}
}
ConfigSwitch {
buttonIcon: "side_navigation"
text: Translation.tr("Depends on sidebars")
checked: Config.options.background.parallax.enableSidebar
onCheckedChanged: {
Config.options.background.parallax.enableSidebar = checked;
}
}
}
ConfigSpinBox {
icon: "loupe"
text: Translation.tr("Preferred wallpaper zoom (%)")
value: Config.options.background.parallax.workspaceZoom * 100
from: 100
to: 150
stepSize: 1
onValueChanged: {
Config.options.background.parallax.workspaceZoom = value / 100;
}
}
}
ContentSection {
icon: "clock_loader_40"
title: Translation.tr("Widget: Clock")
ConfigRow {
Layout.fillWidth: true
ConfigSwitch {
Layout.fillWidth: false
buttonIcon: "check"
text: Translation.tr("Enable")
checked: Config.options.background.widgets.clock.enable
onCheckedChanged: {
Config.options.background.widgets.clock.enable = checked;
}
}
Item {
Layout.fillWidth: true
}
ConfigSelectionArray {
Layout.fillWidth: false
currentValue: Config.options.background.widgets.clock.placementStrategy
onSelected: newValue => {
Config.options.background.widgets.clock.placementStrategy = newValue;
}
options: [
{
displayName: Translation.tr("Draggable"),
icon: "drag_pan",
value: "free"
},
{
displayName: Translation.tr("Least busy"),
icon: "category",
value: "leastBusy"
},
{
displayName: Translation.tr("Most busy"),
icon: "shapes",
value: "mostBusy"
},
]
}
}
ContentSubsection {
title: Translation.tr("Clock style")
ConfigSelectionArray {
currentValue: Config.options.background.widgets.clock.style
onSelected: newValue => {
Config.options.background.widgets.clock.style = newValue;
}
options: [
{
displayName: Translation.tr("Digital"),
icon: "timer_10",
value: "digital"
},
{
displayName: Translation.tr("Cookie"),
icon: "cookie",
value: "cookie"
}
]
}
}
ContentSubsection {
visible: Config.options.background.widgets.clock.style === "digital"
title: Translation.tr("Digital clock settings")
ConfigSwitch {
buttonIcon: "animation"
text: Translation.tr("Animate time change")
checked: Config.options.background.widgets.clock.digital.animateChange
onCheckedChanged: {
Config.options.background.widgets.clock.digital.animateChange = checked;
}
}
}
ContentSubsection {
visible: Config.options.background.widgets.clock.style === "cookie"
title: Translation.tr("Cookie clock settings")
ConfigSwitch {
buttonIcon: "wand_stars"
text: Translation.tr("Auto styling with Gemini")
checked: Config.options.background.widgets.clock.cookie.aiStyling
onCheckedChanged: {
Config.options.background.widgets.clock.cookie.aiStyling = checked;
}
StyledToolTip {
text: Translation.tr("Uses Gemini to categorize the wallpaper then picks a preset based on it.\nYou'll need to set Gemini API key on the left sidebar first.\nImages are downscaled for performance, but just to be safe,\ndo not select wallpapers with sensitive information.")
}
}
ConfigSwitch {
buttonIcon: "airwave"
text: Translation.tr("Use old sine wave cookie implementation")
checked: Config.options.background.widgets.clock.cookie.useSineCookie
onCheckedChanged: {
Config.options.background.widgets.clock.cookie.useSineCookie = checked;
}
StyledToolTip {
text: "Looks a bit softer and more consistent with different number of sides,\nbut has less impressive morphing"
}
}
ConfigSpinBox {
icon: "add_triangle"
text: Translation.tr("Sides")
value: Config.options.background.widgets.clock.cookie.sides
from: 0
to: 40
stepSize: 1
onValueChanged: {
Config.options.background.widgets.clock.cookie.sides = value;
}
}
ConfigSwitch {
buttonIcon: "autoplay"
text: Translation.tr("Constantly rotate")
checked: Config.options.background.widgets.clock.cookie.constantlyRotate
onCheckedChanged: {
Config.options.background.widgets.clock.cookie.constantlyRotate = checked;
}
StyledToolTip {
text: "Makes the clock always rotate. This is extremely expensive\n(expect 50% usage on Intel UHD Graphics) and thus impractical."
}
}
ConfigRow {
ConfigSwitch {
enabled: Config.options.background.widgets.clock.style === "cookie" && Config.options.background.widgets.clock.cookie.dialNumberStyle === "dots" || Config.options.background.widgets.clock.cookie.dialNumberStyle === "full"
buttonIcon: "brightness_7"
text: Translation.tr("Hour marks")
checked: Config.options.background.widgets.clock.cookie.hourMarks
onEnabledChanged: {
checked = Config.options.background.widgets.clock.cookie.hourMarks;
}
onCheckedChanged: {
Config.options.background.widgets.clock.cookie.hourMarks = checked;
}
StyledToolTip {
text: "Can only be turned on using the 'Dots' or 'Full' dial style for aesthetic reasons"
}
}
ConfigSwitch {
enabled: Config.options.background.widgets.clock.style === "cookie" && Config.options.background.widgets.clock.cookie.dialNumberStyle !== "numbers"
buttonIcon: "timer_10"
text: Translation.tr("Digits in the middle")
checked: Config.options.background.widgets.clock.cookie.timeIndicators
onEnabledChanged: {
checked = Config.options.background.widgets.clock.cookie.timeIndicators;
}
onCheckedChanged: {
Config.options.background.widgets.clock.cookie.timeIndicators = checked;
}
StyledToolTip {
text: "Can't be turned on when using 'Numbers' dial style for aesthetic reasons"
}
}
}
}
ContentSubsection {
visible: Config.options.background.widgets.clock.style === "cookie"
title: Translation.tr("Dial style")
ConfigSelectionArray {
currentValue: Config.options.background.widgets.clock.cookie.dialNumberStyle
onSelected: newValue => {
Config.options.background.widgets.clock.cookie.dialNumberStyle = newValue;
if (newValue !== "dots" && newValue !== "full") {
Config.options.background.widgets.clock.cookie.hourMarks = false;
}
if (newValue === "numbers") {
Config.options.background.widgets.clock.cookie.timeIndicators = false;
}
}
options: [
{
displayName: "",
icon: "block",
value: "none"
},
{
displayName: Translation.tr("Dots"),
icon: "graph_6",
value: "dots"
},
{
displayName: Translation.tr("Full"),
icon: "history_toggle_off",
value: "full"
},
{
displayName: Translation.tr("Numbers"),
icon: "counter_1",
value: "numbers"
}
]
}
}
ContentSubsection {
visible: Config.options.background.widgets.clock.style === "cookie"
title: Translation.tr("Hour hand")
ConfigSelectionArray {
currentValue: Config.options.background.widgets.clock.cookie.hourHandStyle
onSelected: newValue => {
Config.options.background.widgets.clock.cookie.hourHandStyle = newValue;
}
options: [
{
displayName: "",
icon: "block",
value: "hide"
},
{
displayName: Translation.tr("Classic"),
icon: "radio",
value: "classic"
},
{
displayName: Translation.tr("Hollow"),
icon: "circle",
value: "hollow"
},
{
displayName: Translation.tr("Fill"),
icon: "eraser_size_5",
value: "fill"
},
]
}
}
ContentSubsection {
visible: Config.options.background.widgets.clock.style === "cookie"
title: Translation.tr("Minute hand")
ConfigSelectionArray {
currentValue: Config.options.background.widgets.clock.cookie.minuteHandStyle
onSelected: newValue => {
Config.options.background.widgets.clock.cookie.minuteHandStyle = newValue;
}
options: [
{
displayName: "",
icon: "block",
value: "hide"
},
{
displayName: Translation.tr("Classic"),
icon: "radio",
value: "classic"
},
{
displayName: Translation.tr("Thin"),
icon: "line_end",
value: "thin"
},
{
displayName: Translation.tr("Medium"),
icon: "eraser_size_2",
value: "medium"
},
{
displayName: Translation.tr("Bold"),
icon: "eraser_size_4",
value: "bold"
},
]
}
}
ContentSubsection {
visible: Config.options.background.widgets.clock.style === "cookie"
title: Translation.tr("Second hand")
ConfigSelectionArray {
currentValue: Config.options.background.widgets.clock.cookie.secondHandStyle
onSelected: newValue => {
Config.options.background.widgets.clock.cookie.secondHandStyle = newValue;
}
options: [
{
displayName: "",
icon: "block",
value: "hide"
},
{
displayName: Translation.tr("Classic"),
icon: "radio",
value: "classic"
},
{
displayName: Translation.tr("Line"),
icon: "line_end",
value: "line"
},
{
displayName: Translation.tr("Dot"),
icon: "adjust",
value: "dot"
},
]
}
}
ContentSubsection {
visible: Config.options.background.widgets.clock.style === "cookie"
title: Translation.tr("Date style")
ConfigSelectionArray {
currentValue: Config.options.background.widgets.clock.cookie.dateStyle
onSelected: newValue => {
Config.options.background.widgets.clock.cookie.dateStyle = newValue;
}
options: [
{
displayName: "",
icon: "block",
value: "hide"
},
{
displayName: Translation.tr("Bubble"),
icon: "bubble_chart",
value: "bubble"
},
{
displayName: Translation.tr("Border"),
icon: "rotate_right",
value: "border"
},
{
displayName: Translation.tr("Rect"),
icon: "rectangle",
value: "rect"
}
]
}
}
ContentSubsection {
title: Translation.tr("Quote")
ConfigSwitch {
buttonIcon: "check"
text: Translation.tr("Enable")
checked: Config.options.background.widgets.clock.quote.enable
onCheckedChanged: {
Config.options.background.widgets.clock.quote.enable = checked;
}
}
MaterialTextArea {
Layout.fillWidth: true
placeholderText: Translation.tr("Quote")
text: Config.options.background.widgets.clock.quote.text
wrapMode: TextEdit.Wrap
onTextChanged: {
Config.options.background.widgets.clock.quote.text = text;
}
}
}
}
ContentSection {
icon: "weather_mix"
title: Translation.tr("Widget: Weather")
ConfigRow {
Layout.fillWidth: true
ConfigSwitch {
Layout.fillWidth: false
buttonIcon: "check"
text: Translation.tr("Enable")
checked: Config.options.background.widgets.weather.enable
onCheckedChanged: {
Config.options.background.widgets.weather.enable = checked;
}
}
Item {
Layout.fillWidth: true
}
ConfigSelectionArray {
Layout.fillWidth: false
currentValue: Config.options.background.widgets.weather.placementStrategy
onSelected: newValue => {
Config.options.background.widgets.weather.placementStrategy = newValue;
}
options: [
{
displayName: Translation.tr("Draggable"),
icon: "drag_pan",
value: "free"
},
{
displayName: Translation.tr("Least busy"),
icon: "category",
value: "leastBusy"
},
{
displayName: Translation.tr("Most busy"),
icon: "shapes",
value: "mostBusy"
},
]
}
}
}
}
@@ -318,17 +318,17 @@ ContentPage {
{ {
displayName: Translation.tr("Normal"), displayName: Translation.tr("Normal"),
icon: "timer_10", icon: "timer_10",
value: '["1","2","3","4","5","6","7","8","9","10"]' value: '[]'
}, },
{ {
displayName: Translation.tr("Japanese"), displayName: Translation.tr("Han chars"),
icon: "square_dot", icon: "square_dot",
value: '["一","二","三","四","五","六","七","八","九","十"]' value: '["一","二","三","四","五","六","七","八","九","十","十一","十二","十三","十四","十五","十六","十七","十八","十九","二十"]'
}, },
{ {
displayName: Translation.tr("Roman"), displayName: Translation.tr("Roman"),
icon: "account_balance", icon: "account_balance",
value: '["I","II","III","IV","V","VI","VII","VIII","IX","X"]' value: '["I","II","III","IV","V","VI","VII","VIII","IX","X","XI","XII","XIII","XIV","XV","XVI","XVII","XVIII","XIX","XX"]'
} }
] ]
} }
@@ -7,440 +7,6 @@ import qs.modules.common.widgets
ContentPage { ContentPage {
forceWidth: true forceWidth: true
ContentSection {
icon: "wallpaper"
title: Translation.tr("Background")
ConfigSwitch {
buttonIcon: "nest_clock_farsight_analog"
text: Translation.tr("Show clock")
checked: Config.options.background.clock.show
onCheckedChanged: {
Config.options.background.clock.show = checked;
}
}
ConfigSpinBox {
icon: "loupe"
text: Translation.tr("Scale (%)")
value: Config.options.background.clock.scale * 100
from: 1
to: 200
stepSize: 2
onValueChanged: {
Config.options.background.clock.scale = value / 100;
}
}
ContentSubsection {
title: Translation.tr("Clock style")
ConfigSelectionArray {
currentValue: Config.options.background.clock.style
onSelected: newValue => {
Config.options.background.clock.style = newValue;
}
options: [
{
displayName: Translation.tr("Simple digital"),
icon: "timer_10",
value: "digital"
},
{
displayName: Translation.tr("Material cookie"),
icon: "cookie",
value: "cookie"
}
]
}
}
ContentSubsection {
visible: Config.options.background.clock.style === "digital"
title: Translation.tr("Digital clock settings")
ConfigSwitch {
buttonIcon: "animation"
text: Translation.tr("Animate time change")
checked: Config.options.background.clock.digital.animateChange
onCheckedChanged: {
Config.options.background.clock.digital.animateChange = checked;
}
}
}
ContentSubsection {
visible: Config.options.background.clock.style === "cookie"
title: Translation.tr("Cookie clock settings")
ConfigSwitch {
buttonIcon: "wand_stars"
text: Translation.tr("Auto styling with Gemini")
checked: Config.options.background.clock.cookie.aiStyling
onCheckedChanged: {
Config.options.background.clock.cookie.aiStyling = checked;
}
StyledToolTip {
text: Translation.tr("Uses Gemini to categorize the wallpaper then picks a preset based on it.\nYou'll need to set Gemini API key on the left sidebar first.\nImages are downscaled for performance, but just to be safe,\ndo not select wallpapers with sensitive information.")
}
}
ConfigSwitch {
buttonIcon: "airwave"
text: Translation.tr("Use old sine wave cookie implementation")
checked: Config.options.background.clock.cookie.useSineCookie
onCheckedChanged: {
Config.options.background.clock.cookie.useSineCookie = checked;
}
StyledToolTip {
text: "Looks a bit softer and more consistent with different number of sides,\nbut has less impressive morphing"
}
}
ConfigSpinBox {
icon: "add_triangle"
text: Translation.tr("Sides")
value: Config.options.background.clock.cookie.sides
from: 0
to: 40
stepSize: 1
onValueChanged: {
Config.options.background.clock.cookie.sides = value;
}
}
ConfigSwitch {
buttonIcon: "autoplay"
text: Translation.tr("Constantly rotate")
checked: Config.options.background.clock.cookie.constantlyRotate
onCheckedChanged: {
Config.options.background.clock.cookie.constantlyRotate = checked;
}
StyledToolTip {
text: "Makes the clock always rotate. This is extremely expensive\n(expect 50% usage on Intel UHD Graphics) and thus impractical."
}
}
ConfigRow {
ConfigSwitch {
enabled: Config.options.background.clock.style === "cookie" && Config.options.background.clock.cookie.dialNumberStyle === "dots" || Config.options.background.clock.cookie.dialNumberStyle === "full"
buttonIcon: "brightness_7"
text: Translation.tr("Hour marks")
checked: Config.options.background.clock.cookie.hourMarks
onEnabledChanged: {
checked = Config.options.background.clock.cookie.hourMarks;
}
onCheckedChanged: {
Config.options.background.clock.cookie.hourMarks = checked;
}
StyledToolTip {
text: "Can only be turned on using the 'Dots' or 'Full' dial style for aesthetic reasons"
}
}
ConfigSwitch {
enabled: Config.options.background.clock.style === "cookie" && Config.options.background.clock.cookie.dialNumberStyle !== "numbers"
buttonIcon: "timer_10"
text: Translation.tr("Digits in the middle")
checked: Config.options.background.clock.cookie.timeIndicators
onEnabledChanged: {
checked = Config.options.background.clock.cookie.timeIndicators;
}
onCheckedChanged: {
Config.options.background.clock.cookie.timeIndicators = checked;
}
StyledToolTip {
text: "Can't be turned on when using 'Numbers' dial style for aesthetic reasons"
}
}
}
}
ContentSubsection {
visible: Config.options.background.clock.style === "cookie"
title: Translation.tr("Dial style")
ConfigSelectionArray {
currentValue: Config.options.background.clock.cookie.dialNumberStyle
onSelected: newValue => {
Config.options.background.clock.cookie.dialNumberStyle = newValue;
if (newValue !== "dots" && newValue !== "full") {
Config.options.background.clock.cookie.hourMarks = false;
}
if (newValue === "numbers") {
Config.options.background.clock.cookie.timeIndicators = false;
}
}
options: [
{
displayName: "",
icon: "block",
value: "none"
},
{
displayName: Translation.tr("Dots"),
icon: "graph_6",
value: "dots"
},
{
displayName: Translation.tr("Full"),
icon: "history_toggle_off",
value: "full"
},
{
displayName: Translation.tr("Numbers"),
icon: "counter_1",
value: "numbers"
}
]
}
}
ContentSubsection {
visible: Config.options.background.clock.style === "cookie"
title: Translation.tr("Hour hand")
ConfigSelectionArray {
currentValue: Config.options.background.clock.cookie.hourHandStyle
onSelected: newValue => {
Config.options.background.clock.cookie.hourHandStyle = newValue;
}
options: [
{
displayName: "",
icon: "block",
value: "hide"
},
{
displayName: Translation.tr("Classic"),
icon: "radio",
value: "classic"
},
{
displayName: Translation.tr("Hollow"),
icon: "circle",
value: "hollow"
},
{
displayName: Translation.tr("Fill"),
icon: "eraser_size_5",
value: "fill"
},
]
}
}
ContentSubsection {
visible: Config.options.background.clock.style === "cookie"
title: Translation.tr("Minute hand")
ConfigSelectionArray {
currentValue: Config.options.background.clock.cookie.minuteHandStyle
onSelected: newValue => {
Config.options.background.clock.cookie.minuteHandStyle = newValue;
}
options: [
{
displayName: "",
icon: "block",
value: "hide"
},
{
displayName: Translation.tr("Classic"),
icon: "radio",
value: "classic"
},
{
displayName: Translation.tr("Thin"),
icon: "line_end",
value: "thin"
},
{
displayName: Translation.tr("Medium"),
icon: "eraser_size_2",
value: "medium"
},
{
displayName: Translation.tr("Bold"),
icon: "eraser_size_4",
value: "bold"
},
]
}
}
ContentSubsection {
visible: Config.options.background.clock.style === "cookie"
title: Translation.tr("Second hand")
ConfigSelectionArray {
currentValue: Config.options.background.clock.cookie.secondHandStyle
onSelected: newValue => {
Config.options.background.clock.cookie.secondHandStyle = newValue;
}
options: [
{
displayName: "",
icon: "block",
value: "hide"
},
{
displayName: Translation.tr("Classic"),
icon: "radio",
value: "classic"
},
{
displayName: Translation.tr("Line"),
icon: "line_end",
value: "line"
},
{
displayName: Translation.tr("Dot"),
icon: "adjust",
value: "dot"
},
]
}
}
ContentSubsection {
visible: Config.options.background.clock.style === "cookie"
title: Translation.tr("Date style")
ConfigSelectionArray {
currentValue: Config.options.background.clock.cookie.dateStyle
onSelected: newValue => {
Config.options.background.clock.cookie.dateStyle = newValue;
}
options: [
{
displayName: "",
icon: "block",
value: "hide"
},
{
displayName: Translation.tr("Bubble"),
icon: "bubble_chart",
value: "bubble"
},
{
displayName: Translation.tr("Border"),
icon: "rotate_right",
value: "border"
},
{
displayName: Translation.tr("Rect"),
icon: "rectangle",
value: "rect"
}
]
}
}
ContentSubsection {
title: Translation.tr("Quote settings")
ConfigSwitch {
buttonIcon: "format_quote"
text: Translation.tr("Show quote")
checked: Config.options.background.showQuote
onCheckedChanged: {
Config.options.background.showQuote = checked;
}
}
MaterialTextArea {
Layout.fillWidth: true
placeholderText: Translation.tr("Quote")
text: Config.options.background.quote
wrapMode: TextEdit.Wrap
onTextChanged: {
Config.options.background.quote = text;
}
}
}
ContentSubsection {
title: Translation.tr("Wallpaper parallax")
ConfigSwitch {
buttonIcon: "unfold_more_double"
text: Translation.tr("Vertical")
checked: Config.options.background.parallax.vertical
onCheckedChanged: {
Config.options.background.parallax.vertical = checked;
}
}
ConfigRow {
uniform: true
ConfigSwitch {
buttonIcon: "counter_1"
text: Translation.tr("Depends on workspace")
checked: Config.options.background.parallax.enableWorkspace
onCheckedChanged: {
Config.options.background.parallax.enableWorkspace = checked;
}
}
ConfigSwitch {
buttonIcon: "side_navigation"
text: Translation.tr("Depends on sidebars")
checked: Config.options.background.parallax.enableSidebar
onCheckedChanged: {
Config.options.background.parallax.enableSidebar = checked;
}
}
}
ConfigSpinBox {
icon: "loupe"
text: Translation.tr("Preferred wallpaper zoom (%)")
value: Config.options.background.parallax.workspaceZoom * 100
from: 100
to: 150
stepSize: 1
onValueChanged: {
Config.options.background.parallax.workspaceZoom = value / 100;
}
}
}
}
ContentSection {
icon: "point_scan"
title: Translation.tr("Crosshair overlay")
MaterialTextArea {
Layout.fillWidth: true
placeholderText: Translation.tr("Crosshair code (in Valorant's format)")
text: Config.options.crosshair.code
wrapMode: TextEdit.Wrap
onTextChanged: {
Config.options.crosshair.code = text;
}
}
RowLayout {
StyledText {
Layout.leftMargin: 10
color: Appearance.colors.colSubtext
font.pixelSize: Appearance.font.pixelSize.smallie
text: Translation.tr("Press Super+G to toggle appearance")
}
Item {
Layout.fillWidth: true
}
RippleButtonWithIcon {
id: editorButton
buttonRadius: Appearance.rounding.full
materialIcon: "open_in_new"
mainText: Translation.tr("Open editor")
onClicked: {
Qt.openUrlExternally(`https://www.vcrdb.net/builder?c=${Config.options.crosshair.code}`);
}
StyledToolTip {
text: "www.vcrdb.net"
}
}
}
}
ContentSection { ContentSection {
icon: "call_to_action" icon: "call_to_action"
title: Translation.tr("Dock") title: Translation.tr("Dock")
@@ -609,6 +175,67 @@ ContentPage {
} }
} }
ContentSection {
icon: "select_window"
title: Translation.tr("Overlay: General")
ConfigSwitch {
buttonIcon: "high_density"
text: Translation.tr("Enable opening zoom animation")
checked: Config.options.overlay.openingZoomAnimation
onCheckedChanged: {
Config.options.overlay.openingZoomAnimation = checked;
}
}
ConfigSwitch {
buttonIcon: "texture"
text: Translation.tr("Darken screen")
checked: Config.options.overlay.darkenScreen
onCheckedChanged: {
Config.options.overlay.darkenScreen = checked;
}
}
}
ContentSection {
icon: "point_scan"
title: Translation.tr("Overlay: Crosshair")
MaterialTextArea {
Layout.fillWidth: true
placeholderText: Translation.tr("Crosshair code (in Valorant's format)")
text: Config.options.crosshair.code
wrapMode: TextEdit.Wrap
onTextChanged: {
Config.options.crosshair.code = text;
}
}
RowLayout {
StyledText {
Layout.leftMargin: 10
color: Appearance.colors.colSubtext
font.pixelSize: Appearance.font.pixelSize.smallie
text: Translation.tr("Press Super+G to open the overlay and pin the crosshair")
}
Item {
Layout.fillWidth: true
}
RippleButtonWithIcon {
id: editorButton
buttonRadius: Appearance.rounding.full
materialIcon: "open_in_new"
mainText: Translation.tr("Open editor")
onClicked: {
Qt.openUrlExternally(`https://www.vcrdb.net/builder?c=${Config.options.crosshair.code}`);
}
StyledToolTip {
text: "www.vcrdb.net"
}
}
}
}
ContentSection { ContentSection {
icon: "screenshot_frame_2" icon: "screenshot_frame_2"
title: Translation.tr("Region selector (screen snipping/Google Lens)") title: Translation.tr("Region selector (screen snipping/Google Lens)")
@@ -85,6 +85,31 @@ ContentPage {
} }
ContentSection {
icon: "file_open"
title: Translation.tr("Save paths")
MaterialTextArea {
Layout.fillWidth: true
placeholderText: Translation.tr("Video Recording Path")
text: Config.options.screenRecord.savePath
wrapMode: TextEdit.Wrap
onTextChanged: {
Config.options.screenRecord.savePath = text;
}
}
MaterialTextArea {
Layout.fillWidth: true
placeholderText: Translation.tr("Screenshot Path (leave empty to just copy)")
text: Config.options.screenSnip.savePath
wrapMode: TextEdit.Wrap
onTextChanged: {
Config.options.screenSnip.savePath = text;
}
}
}
ContentSection { ContentSection {
icon: "search" icon: "search"
title: Translation.tr("Search") title: Translation.tr("Search")
@@ -329,7 +329,7 @@ Rectangle {
segmentContent: thisBlock.content segmentContent: thisBlock.content
messageData: root.messageData messageData: root.messageData
done: root.messageData?.done ?? false done: root.messageData?.done ?? false
forceDisableChunkSplitting: root.messageData.content.includes("```") forceDisableChunkSplitting: root.messageData?.content.includes("```") ?? true
} } } }
} }
} }
@@ -159,7 +159,7 @@ ColumnLayout {
selectedTextColor: Appearance.m3colors.m3onSecondaryContainer selectedTextColor: Appearance.m3colors.m3onSecondaryContainer
selectionColor: Appearance.colors.colSecondaryContainer selectionColor: Appearance.colors.colSecondaryContainer
wrapMode: TextEdit.Wrap wrapMode: TextEdit.Wrap
color: messageData.thinking ? Appearance.colors.colSubtext : Appearance.colors.colOnLayer1 color: root.messageData?.thinking ? Appearance.colors.colSubtext : Appearance.colors.colOnLayer1
textFormat: renderMarkdown ? TextEdit.MarkdownText : TextEdit.PlainText textFormat: renderMarkdown ? TextEdit.MarkdownText : TextEdit.PlainText
text: modelData text: modelData
@@ -23,7 +23,7 @@ AndroidQuickToggleButton {
interval: 300 interval: 300
repeat: false repeat: false
onTriggered: { onTriggered: {
Hyprland.dispatch("global quickshell:regionScreenshot") Quickshell.execDetached(["qs", "-p", Quickshell.shellPath(""), "ipc", "call", "region", "screenshot"]);
} }
} }
@@ -11,87 +11,14 @@ import Quickshell.Services.Pipewire
WindowDialog { WindowDialog {
id: root id: root
property bool isSink: true property bool isSink: true
function correctType(node) {
return (node.isSink === root.isSink) && node.audio
}
readonly property list<var> appPwNodes: Pipewire.nodes.values.filter((node) => { // Should be list<PwNode> but it breaks ScriptModel
return root.correctType(node) && node.isStream
})
readonly property bool hasApps: appPwNodes.length > 0
backgroundHeight: 700 backgroundHeight: 700
WindowDialogTitle { WindowDialogTitle {
text: root.isSink ? Translation.tr("Audio output") : Translation.tr("Audio input") text: root.isSink ? Translation.tr("Audio output") : Translation.tr("Audio input")
} }
WindowDialogSectionHeader { VolumeDialogContent {
visible: root.hasApps isSink: root.isSink
text: Translation.tr("Applications")
}
WindowDialogSeparator {
visible: root.hasApps
Layout.topMargin: -22
Layout.leftMargin: 0
Layout.rightMargin: 0
}
DialogSectionListView {
visible: root.hasApps
Layout.fillHeight: true
model: ScriptModel {
values: root.appPwNodes
}
delegate: VolumeMixerEntry {
anchors {
left: parent?.left
right: parent?.right
}
required property var modelData
node: modelData
}
}
WindowDialogSectionHeader {
text: Translation.tr("Devices")
}
WindowDialogSeparator {
Layout.topMargin: -22
Layout.leftMargin: 0
Layout.rightMargin: 0
}
DialogSectionListView {
Layout.fillHeight: !root.hasApps
Layout.preferredHeight: 180
model: ScriptModel {
values: Pipewire.nodes.values.filter(node => {
return root.correctType(node) && !node.isStream
})
}
delegate: StyledRadioButton {
id: radioButton
required property var modelData
anchors {
left: parent?.left
right: parent?.right
}
description: modelData.description
checked: modelData.id === (root.isSink ? Pipewire.preferredDefaultAudioSink?.id : Pipewire.preferredDefaultAudioSource?.id)
onCheckedChanged: {
if (!checked) return;
if (root.isSink) {
Pipewire.preferredDefaultAudioSink = modelData
} else {
Pipewire.preferredDefaultAudioSource = modelData
}
}
}
} }
WindowDialogSeparator { WindowDialogSeparator {
@@ -117,20 +44,4 @@ WindowDialog {
onClicked: root.dismiss() onClicked: root.dismiss()
} }
} }
component DialogSectionListView: StyledListView {
Layout.fillWidth: true
Layout.topMargin: -22
Layout.bottomMargin: -16
Layout.leftMargin: -Appearance.rounding.large
Layout.rightMargin: -Appearance.rounding.large
topMargin: 12
bottomMargin: 12
leftMargin: 20
rightMargin: 20
clip: true
spacing: 4
animateAppearance: false
}
} }
@@ -0,0 +1,107 @@
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.Pipewire
ColumnLayout {
id: root
required property bool isSink
function correctType(node) {
return (node.isSink === root.isSink) && node.audio
}
readonly property list<var> appPwNodes: Pipewire.nodes.values.filter((node) => { // Should be list<PwNode> but it breaks ScriptModel
return root.correctType(node) && node.isStream
})
readonly property bool hasApps: appPwNodes.length > 0
spacing: 16
WindowDialogSectionHeader {
visible: root.hasApps
text: Translation.tr("Applications")
}
WindowDialogSeparator {
visible: root.hasApps
Layout.topMargin: -22
Layout.leftMargin: 0
Layout.rightMargin: 0
}
DialogSectionListView {
visible: root.hasApps
Layout.fillHeight: true
model: ScriptModel {
values: root.appPwNodes
}
delegate: VolumeMixerEntry {
anchors {
left: parent?.left
right: parent?.right
}
required property var modelData
node: modelData
}
}
WindowDialogSectionHeader {
text: Translation.tr("Devices")
}
WindowDialogSeparator {
Layout.topMargin: -22
Layout.leftMargin: 0
Layout.rightMargin: 0
}
DialogSectionListView {
Layout.fillHeight: !root.hasApps
Layout.preferredHeight: 180
model: ScriptModel {
values: Pipewire.nodes.values.filter(node => {
return root.correctType(node) && !node.isStream
})
}
delegate: StyledRadioButton {
id: radioButton
required property var modelData
anchors {
left: parent?.left
right: parent?.right
}
description: modelData.description
checked: modelData.id === (root.isSink ? Pipewire.preferredDefaultAudioSink?.id : Pipewire.preferredDefaultAudioSource?.id)
onCheckedChanged: {
if (!checked) return;
if (root.isSink) {
Pipewire.preferredDefaultAudioSink = modelData
} else {
Pipewire.preferredDefaultAudioSource = modelData
}
}
}
}
component DialogSectionListView: StyledListView {
Layout.fillWidth: true
Layout.topMargin: -22
Layout.bottomMargin: -16
Layout.leftMargin: -Appearance.rounding.large
Layout.rightMargin: -Appearance.rounding.large
topMargin: 12
bottomMargin: 12
leftMargin: 20
rightMargin: 20
clip: true
spacing: 4
animateAppearance: false
}
}
@@ -36,7 +36,7 @@ Item {
hoverEnabled: true hoverEnabled: true
acceptedButtons: Qt.NoButton acceptedButtons: Qt.NoButton
Bar.ClockWidgetTooltip { Bar.ClockWidgetPopup {
hoverTarget: mouseArea hoverTarget: mouseArea
} }
} }
@@ -74,30 +74,13 @@ MouseArea {
anchors.centerIn: parent anchors.centerIn: parent
spacing: 4 spacing: 4
Row { Bar.StyledPopupHeaderRow {
spacing: 4 icon: "music_note"
label: Translation.tr("Media")
MaterialSymbol {
anchors.verticalCenter: parent.verticalCenter
fill: 0
font.weight: Font.Medium
text: "music_note"
iconSize: Appearance.font.pixelSize.large
color: Appearance.colors.colOnSurfaceVariant
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: "Media"
font {
weight: Font.Medium
pixelSize: Appearance.font.pixelSize.normal
}
color: Appearance.colors.colOnSurfaceVariant
}
} }
StyledText { StyledText {
color: Appearance.colors.colOnSurfaceVariant
text: `${cleanedTitle}${activePlayer?.trackArtist ? '\n' + activePlayer.trackArtist : ''}` text: `${cleanedTitle}${activePlayer?.trackArtist ? '\n' + activePlayer.trackArtist : ''}`
} }
} }
@@ -18,7 +18,7 @@ def center_crop(img, target_w, target_h):
y2 = y1 + target_h y2 = y1 + target_h
return img[y1:y2, x1:x2] return img[y1:y2, x1:x2]
def find_least_busy_region(image_path, region_width=300, region_height=200, screen_width=None, screen_height=None, verbose=False, stride=2, screen_mode="fill", horizontal_padding=50, vertical_padding=50): def find_least_busy_region(image_path, region_width=300, region_height=200, screen_width=None, screen_height=None, verbose=False, stride=2, screen_mode="fill", horizontal_padding=50, vertical_padding=50, busiest=False):
img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE) img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
if img is None: if img is None:
raise FileNotFoundError(f"Image not found: {image_path}") raise FileNotFoundError(f"Image not found: {image_path}")
@@ -77,7 +77,9 @@ def find_least_busy_region(image_path, region_width=300, region_height=200, scre
total += ii[y1-1, x1-1] total += ii[y1-1, x1-1]
return total return total
min_var = None min_var = None
max_var = None
min_coords = (horizontal_padding, vertical_padding) min_coords = (horizontal_padding, vertical_padding)
max_coords = (horizontal_padding, vertical_padding)
area = region_width * region_height area = region_width * region_height
x_start = horizontal_padding x_start = horizontal_padding
y_start = vertical_padding y_start = vertical_padding
@@ -100,7 +102,13 @@ def find_least_busy_region(image_path, region_width=300, region_height=200, scre
if (min_var is None) or (var < min_var): if (min_var is None) or (var < min_var):
min_var = var min_var = var
min_coords = (x, y) min_coords = (x, y)
return min_coords, min_var if (max_var is None) or (var > max_var):
max_var = var
max_coords = (x, y)
if busiest:
return max_coords, max_var
else:
return min_coords, min_var
def find_largest_region(image_path, screen_width=None, screen_height=None, verbose=False, stride=2, screen_mode="fill", threshold=100.0, aspect_ratio=1.0, horizontal_padding=50, vertical_padding=50): def find_largest_region(image_path, screen_width=None, screen_height=None, verbose=False, stride=2, screen_mode="fill", threshold=100.0, aspect_ratio=1.0, horizontal_padding=50, vertical_padding=50):
img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE) img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
@@ -313,6 +321,7 @@ def main():
parser.add_argument("--aspect-ratio", type=float, default=1.78, help="Aspect ratio (width/height) for largest region mode") parser.add_argument("--aspect-ratio", type=float, default=1.78, help="Aspect ratio (width/height) for largest region mode")
parser.add_argument("--horizontal-padding", "-hp", type=int, default=50, help="Minimum horizontal distance from region to image edge") parser.add_argument("--horizontal-padding", "-hp", type=int, default=50, help="Minimum horizontal distance from region to image edge")
parser.add_argument("--vertical-padding", "-vp", type=int, default=50, help="Minimum vertical distance from region to image edge") parser.add_argument("--vertical-padding", "-vp", type=int, default=50, help="Minimum vertical distance from region to image edge")
parser.add_argument("--busiest", action="store_true", help="Find the busiest region instead of the least busy")
args = parser.parse_args() args = parser.parse_args()
if args.largest_region: if args.largest_region:
@@ -363,7 +372,8 @@ def main():
stride=args.stride, stride=args.stride,
screen_mode=args.screen_mode, screen_mode=args.screen_mode,
horizontal_padding=args.horizontal_padding, horizontal_padding=args.horizontal_padding,
vertical_padding=args.vertical_padding vertical_padding=args.vertical_padding,
busiest=args.busiest
) )
if args.visual_output: if args.visual_output:
draw_region(args.image_path, coords, region_width=args.width, region_height=args.height, screen_width=args.screen_width, screen_height=args.screen_height, screen_mode=args.screen_mode) draw_region(args.image_path, coords, region_width=args.width, region_height=args.height, screen_width=args.screen_width, screen_height=args.screen_height, screen_mode=args.screen_mode)
@@ -1,5 +1,18 @@
#!/usr/bin/env bash #!/usr/bin/env bash
CONFIG_FILE="$HOME/.config/illogical-impulse/config.json"
JSON_PATH=".screenRecord.savePath"
CUSTOM_PATH=$(jq -r "$JSON_PATH" "$CONFIG_FILE" 2>/dev/null)
RECORDING_DIR=""
if [[ -n "$CUSTOM_PATH" ]]; then
RECORDING_DIR="$CUSTOM_PATH"
else
RECORDING_DIR="$HOME/Videos" # Use default path
fi
getdate() { getdate() {
date '+%Y-%m-%d_%H.%M.%S' date '+%Y-%m-%d_%H.%M.%S'
} }
@@ -10,12 +23,8 @@ getactivemonitor() {
hyprctl monitors -j | jq -r '.[] | select(.focused == true) | .name' hyprctl monitors -j | jq -r '.[] | select(.focused == true) | .name'
} }
xdgvideo="$(xdg-user-dir VIDEOS)" mkdir -p "$RECORDING_DIR"
if [[ $xdgvideo = "$HOME" ]]; then cd "$RECORDING_DIR" || exit
unset xdgvideo
fi
mkdir -p "${xdgvideo:-$HOME/Videos}"
cd "${xdgvideo:-$HOME/Videos}" || exit
# parse --region <value> without modifying $@ so other flags like --fullscreen still work # parse --region <value> without modifying $@ so other flags like --fullscreen still work
ARGS=("$@") ARGS=("$@")
@@ -31,6 +31,25 @@ Singleton {
property real timeToEmpty: UPower.displayDevice.timeToEmpty property real timeToEmpty: UPower.displayDevice.timeToEmpty
property real timeToFull: UPower.displayDevice.timeToFull property real timeToFull: UPower.displayDevice.timeToFull
property real health: (function() {
const devList = UPower.devices.values;
for (let i = 0; i < devList.length; ++i) {
const dev = devList[i];
if (dev.isLaptopBattery && dev.healthSupported) {
const health = dev.healthPercentage;
if (health === 0) {
return 0.01;
} else if (health < 1) {
return health * 100;
} else {
return health;
}
}
}
return 0;
})()
onIsLowAndNotChargingChanged: { onIsLowAndNotChargingChanged: {
if (!root.available || !isLowAndNotCharging) return; if (!root.available || !isLowAndNotCharging) return;
Quickshell.execDetached([ Quickshell.execDetached([
@@ -38,7 +57,8 @@ Singleton {
Translation.tr("Low battery"), Translation.tr("Low battery"),
Translation.tr("Consider plugging in your device"), Translation.tr("Consider plugging in your device"),
"-u", "critical", "-u", "critical",
"-a", "Shell" "-a", "Shell",
"--hint=int:transient:1",
]) ])
if (root.soundEnabled) Audio.playSystemSound("dialog-warning"); if (root.soundEnabled) Audio.playSystemSound("dialog-warning");
@@ -51,7 +71,8 @@ Singleton {
Translation.tr("Critically low battery"), Translation.tr("Critically low battery"),
Translation.tr("Please charge!\nAutomatic suspend triggers at %1%").arg(Config.options.battery.suspend), Translation.tr("Please charge!\nAutomatic suspend triggers at %1%").arg(Config.options.battery.suspend),
"-u", "critical", "-u", "critical",
"-a", "Shell" "-a", "Shell",
"--hint=int:transient:1",
]); ]);
if (root.soundEnabled) Audio.playSystemSound("suspend-error"); if (root.soundEnabled) Audio.playSystemSound("suspend-error");
@@ -69,7 +90,8 @@ Singleton {
"notify-send", "notify-send",
Translation.tr("Battery full"), Translation.tr("Battery full"),
Translation.tr("Please unplug the charger"), Translation.tr("Please unplug the charger"),
"-a", "Shell" "-a", "Shell",
"--hint=int:transient:1",
]); ]);
if (root.soundEnabled) Audio.playSystemSound("complete"); if (root.soundEnabled) Audio.playSystemSound("complete");
@@ -108,7 +108,7 @@ Singleton {
} }
} }
onExited: (exitCode, exitStatus) => { onExited: (exitCode, exitStatus) => {
console.log("[KeyringStorage] Keyring data fetch process exited with code:", exitCode); // console.log("[KeyringStorage] Keyring data fetch process exited with code:", exitCode);
if (exitCode === 1) { if (exitCode === 1) {
console.error("[KeyringStorage] Entry not found, initializing."); console.error("[KeyringStorage] Entry not found, initializing.");
root.keyringData = {}; root.keyringData = {};
@@ -25,6 +25,7 @@ Singleton {
"text": action.text, "text": action.text,
})) ?? [] })) ?? []
property bool popup: false property bool popup: false
property bool isTransient: notification?.hints.transient ?? false
property string appIcon: notification?.appIcon ?? "" property string appIcon: notification?.appIcon ?? ""
property string appName: notification?.appName ?? "" property string appName: notification?.appName ?? ""
property string body: notification?.body ?? "" property string body: notification?.body ?? ""
@@ -63,7 +64,11 @@ Singleton {
interval: 7000 interval: 7000
running: true running: true
onTriggered: () => { onTriggered: () => {
root.timeoutNotification(notificationId); const index = root.list.findIndex((notif) => notif.notificationId === notificationId);
const notifObject = root.list[index];
print("[Notifications] Notification timer triggered for ID: " + notificationId + ", transient: " + notifObject?.isTransient);
if (notifObject.isTransient) root.discardNotification(notificationId);
else root.timeoutNotification(notificationId);
destroy() destroy()
} }
} }
+5 -1
View File
@@ -31,7 +31,6 @@ ApplicationWindow {
{ {
name: Translation.tr("General"), name: Translation.tr("General"),
icon: "browse", icon: "browse",
iconRotation: 180,
component: "modules/settings/GeneralConfig.qml" component: "modules/settings/GeneralConfig.qml"
}, },
{ {
@@ -40,6 +39,11 @@ ApplicationWindow {
iconRotation: 180, iconRotation: 180,
component: "modules/settings/BarConfig.qml" component: "modules/settings/BarConfig.qml"
}, },
{
name: Translation.tr("Background"),
icon: "texture",
component: "modules/settings/BackgroundConfig.qml"
},
{ {
name: Translation.tr("Interface"), name: Translation.tr("Interface"),
icon: "bottom_app_bar", icon: "bottom_app_bar",
+3 -3
View File
@@ -11,7 +11,6 @@ import qs.modules.common
import qs.modules.background import qs.modules.background
import qs.modules.bar import qs.modules.bar
import qs.modules.cheatsheet import qs.modules.cheatsheet
import qs.modules.crosshair
import qs.modules.dock import qs.modules.dock
import qs.modules.lock import qs.modules.lock
import qs.modules.mediaControls import qs.modules.mediaControls
@@ -25,6 +24,7 @@ import qs.modules.screenCorners
import qs.modules.sessionScreen import qs.modules.sessionScreen
import qs.modules.sidebarLeft import qs.modules.sidebarLeft
import qs.modules.sidebarRight import qs.modules.sidebarRight
import qs.modules.overlay
import qs.modules.verticalBar import qs.modules.verticalBar
import qs.modules.wallpaperSelector import qs.modules.wallpaperSelector
@@ -39,7 +39,6 @@ ShellRoot {
property bool enableBar: true property bool enableBar: true
property bool enableBackground: true property bool enableBackground: true
property bool enableCheatsheet: true property bool enableCheatsheet: true
property bool enableCrosshair: true
property bool enableDock: true property bool enableDock: true
property bool enableLock: true property bool enableLock: true
property bool enableMediaControls: true property bool enableMediaControls: true
@@ -47,6 +46,7 @@ ShellRoot {
property bool enablePolkit: true property bool enablePolkit: true
property bool enableOnScreenDisplay: true property bool enableOnScreenDisplay: true
property bool enableOnScreenKeyboard: true property bool enableOnScreenKeyboard: true
property bool enableOverlay: true
property bool enableOverview: true property bool enableOverview: true
property bool enableRegionSelector: true property bool enableRegionSelector: true
property bool enableReloadPopup: true property bool enableReloadPopup: true
@@ -70,13 +70,13 @@ ShellRoot {
LazyLoader { active: enableBar && Config.ready && !Config.options.bar.vertical; component: Bar {} } LazyLoader { active: enableBar && Config.ready && !Config.options.bar.vertical; component: Bar {} }
LazyLoader { active: enableBackground; component: Background {} } LazyLoader { active: enableBackground; component: Background {} }
LazyLoader { active: enableCheatsheet; component: Cheatsheet {} } LazyLoader { active: enableCheatsheet; component: Cheatsheet {} }
LazyLoader { active: enableCrosshair; component: Crosshair {} }
LazyLoader { active: enableDock && Config.options.dock.enable; component: Dock {} } LazyLoader { active: enableDock && Config.options.dock.enable; component: Dock {} }
LazyLoader { active: enableLock; component: Lock {} } LazyLoader { active: enableLock; component: Lock {} }
LazyLoader { active: enableMediaControls; component: MediaControls {} } LazyLoader { active: enableMediaControls; component: MediaControls {} }
LazyLoader { active: enableNotificationPopup; component: NotificationPopup {} } LazyLoader { active: enableNotificationPopup; component: NotificationPopup {} }
LazyLoader { active: enableOnScreenDisplay; component: OnScreenDisplay {} } LazyLoader { active: enableOnScreenDisplay; component: OnScreenDisplay {} }
LazyLoader { active: enableOnScreenKeyboard; component: OnScreenKeyboard {} } LazyLoader { active: enableOnScreenKeyboard; component: OnScreenKeyboard {} }
LazyLoader { active: enableOverlay; component: Overlay {} }
LazyLoader { active: enableOverview; component: Overview {} } LazyLoader { active: enableOverview; component: Overview {} }
LazyLoader { active: enablePolkit; component: Polkit {} } LazyLoader { active: enablePolkit; component: Polkit {} }
LazyLoader { active: enableRegionSelector; component: RegionSelector {} } LazyLoader { active: enableRegionSelector; component: RegionSelector {} }
+53 -5
View File
@@ -3,14 +3,23 @@
- See also [Install scripts | illogical-impulse](https://ii.clsty.link/en/dev/inst-script/) - See also [Install scripts | illogical-impulse](https://ii.clsty.link/en/dev/inst-script/)
- See also [#1061](https://github.com/end-4/dots-hyprland/issues/1061) - See also [#1061](https://github.com/end-4/dots-hyprland/issues/1061)
**NOTE: The sdata/dist-nix is not for NixOS but every distro, using Nix and home-manager.** **NOTE: The `dist-nix` is not for NixOS but every distro, using Nix and home-manager.**
As we all know Nix and Home-manager has two major functionalities:
- Handling dependencies (i.e. package installation)
- Handling dotfiles
They are discussed in following sections.
# Handling dependencies
## Status
Partially works. See [Discussion #2382](https://github.com/end-4/dots-hyprland/discussions/2382).
## plan ## plan
Note that this script must be idempotent. Note that this script must be idempotent.
TODO: TODO:
- [ ] Fix all TODOs inside `dist-nix`. - [ ] Fix all TODOs inside `dist-nix`. ([search online](https://github.com/search?q=repo%3Aend-4%2Fdots-hyprland+path%3A%2F%5Esdata%5C%2Fdist-nix%5C%2F%2F+TODO&type=code))
- [ ] Warn user if inode-limited filesystem (typically ext4) is used. - [ ] Since Nix uses a large number of inodes, need to warn user if inode-limited filesystem (typically ext4) is used.
- [ ] Deal with error when running `systemctl --user enable ydotool --now`: - [ ] Deal with error when running `systemctl --user enable ydotool --now`:
```plain ```plain
Failed to connect to user scope bus via local transport: $DBUS_SESSION_BUS_ADDRESS and $XDG_RUNTIME_DIR not defined (consider using --machine=<user>@.host --user to connect to bus of other user) Failed to connect to user scope bus via local transport: $DBUS_SESSION_BUS_ADDRESS and $XDG_RUNTIME_DIR not defined (consider using --machine=<user>@.host --user to connect to bus of other user)
@@ -23,9 +32,48 @@ On non-NixOS distros, programs using PAM (typically screen locker) will not work
- One problem is that Debian(-based) distros use modified version of PAM which supports `@include` directive in `/etc/pam.d` config files but the PAM from Nix does not support it, see [this comment](https://github.com/NixOS/nixpkgs/issues/128523#issuecomment-1086106614). - One problem is that Debian(-based) distros use modified version of PAM which supports `@include` directive in `/etc/pam.d` config files but the PAM from Nix does not support it, see [this comment](https://github.com/NixOS/nixpkgs/issues/128523#issuecomment-1086106614).
- Another problem is the location of a suid helper binary that is necessary, see [this comment](https://github.com/end-4/dots-hyprland/issues/1061#issuecomment-3403195230). - Another problem is the location of a suid helper binary that is necessary, see [this comment](https://github.com/end-4/dots-hyprland/issues/1061#issuecomment-3403195230).
The problem could be solved by using the system-provided libpam instead. As [commented](https://github.com/end-4/dots-hyprland/issues/1061#issuecomment-3403195230) by @Cu3PO42 , both the problem could be solved by using the system-provided libpam instead.
See also https://github.com/caelestia-dots/shell/issues/668 See also [caelestia-dots/shell#668](https://github.com/caelestia-dots/shell/issues/668).
### NixGL ### NixGL
On non-NixOS distros, packages installed via home-manager have problem accessing GPU, especially Hyprland because it requires GPU acceleration to launch. `nixGL` should be used to address the problem. On non-NixOS distros, packages installed via home-manager have problem accessing GPU, especially Hyprland because it requires GPU acceleration to launch. `nixGL` should be used to address the problem.
# Handling dot files
## Status
Paused, until some suitable method has been confirmed to meet the requirements below.
## Requirements
About handling the dotfiles, i.e. `dots/`, if we are doing this using Nix then the following requirements must be fulfilled.
**1. Allow modifications over the existing dotfiles.**
Current state of `./setup install`:
- After finishing running `./setup install`, users can modify any dotfiles in a traditional way, and if they run `./setup install` again to update then they need to skip the steps which overwrite the targets that they have modified and later sync the upgrade manually for such targets by themselves.
- For Hyprland, specially we have a `custom` folder along with `~/.config/hypr/hyprland.conf` which will only get overwritten the first time but not the later times running `./setup install`.
- This works but is not elegant. An experimental solution is using yaml config to store the selected behavior for each target, see [#2137](https://github.com/end-4/dots-hyprland/issues/2137).
If we use Nix to handle dotfiles, then it must be at least better than the current state described above, mainly in terms of convenience and automation.
**2. Allow choosing targets.**
This is similar to the above. For example user may want to use their own `~/.config/foot` instead of the files under `dots/.config/foot` entirely.
**3. Easy developing dotfiles or at least not worse than current state.**
About the current state:
- @clsty: "If I were the one who develops the dotfiles, I will make changes to the local Git repo `dots-hyprland` and rerun `./setup install-files -f` to apply the changes to observe the outcome."
- @end-4 (who develops the dots; see [comment](https://github.com/end-4/dots-hyprland/pull/2278#issuecomment-3454929577)): "I modify my local copy of stuff, copy the relevant parts over, optionally selectively pick changes then commit. It's.... the most obvious way but I guess not necessarily the cleanest"
If we use Nix to handle dotfiles, then it must be at least better than the current state described above, mainly in terms of convenience and automation.
**4. Others**
Find out a good method to avoid what @end-4 [mentioned](https://github.com/end-4/dots-hyprland/issues/1061#issuecomment-2954725029):
> About home-manager, from my limited understanding of and experience with it, any change to the config files require a rebuild right? If this is indeed the case, switching entirely to this is not okay. Having to wait 20 seconds for each change is absurd.
Some information may help, e.g. @darsh032 [commented](https://github.com/end-4/dots-hyprland/issues/1061#issuecomment-3336839862):
> I mean thats not really needed you can use mkOutOfStoreSymlink or use hjem-impure to change the configs without rebuilding
And also the "hmrice" [mentioned](https://github.com/end-4/dots-hyprland/issues/1061#issuecomment-3353345504) by @Markus328 , and the `flake.nix` (for quickshell only) [mentioned](https://github.com/end-4/dots-hyprland/issues/1061#issuecomment-3354387126) by @darsh032 .
+3 -2
View File
@@ -18,8 +18,9 @@
# "org.freedesktop.impl.portal.ScreenCast" = [ "gnome" ]; # "org.freedesktop.impl.portal.ScreenCast" = [ "gnome" ];
#}; #};
}; };
# The following seems to generate ~/.config/fontconfig conflicting with the one under dots/ # Note: The following generate files under ~/.config/fontconfig/conf.d/
#fonts.fontconfig.enable = true; # fontconfig may rely on this to properly find fonts installed via Nix.
fonts.fontconfig.enable = true;
wayland.windowManager.hyprland = { wayland.windowManager.hyprland = {
## Make sure home-manager not generate ~/.config/hypr/hyprland.conf ## Make sure home-manager not generate ~/.config/hypr/hyprland.conf