Merge remote-tracking branch 'origin/main' into addon-i18n

This commit is contained in:
月月
2025-07-17 08:41:34 +08:00
29 changed files with 459 additions and 452 deletions
@@ -0,0 +1,223 @@
pragma ComponentBehavior: Bound
import "root:/"
import "root:/modules/common"
import "root:/modules/common/widgets"
import "root:/services"
import "root:/modules/common/functions/color_utils.js" as ColorUtils
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Hyprland
Scope {
id: root
readonly property bool fixedClockPosition: Config.options.background.fixedClockPosition
readonly property real fixedClockX: Config.options.background.clockX
readonly property real fixedClockY: Config.options.background.clockY
Variants {
// For each monitor
model: Quickshell.screens
PanelWindow {
id: bgRoot
required property var modelData
// Workspaces
property HyprlandMonitor monitor: Hyprland.monitorFor(modelData)
property list<var> relevantWindows: HyprlandData.windowList.filter(win => win.monitor == monitor.id && win.workspace.id >= 0).sort((a, b) => a.workspace.id - b.workspace.id)
property int firstWorkspaceId: relevantWindows[0]?.workspace.id || 1
property int lastWorkspaceId: relevantWindows[relevantWindows.length - 1]?.workspace.id || 10
// Wallpaper
property string wallpaperPath: Config.options.background.wallpaperPath
property real preferredWallpaperScale: Config.options.background.parallax.workspaceZoom
property real effectiveWallpaperScale: 1 // Some reasonable init value, to be updated
property int wallpaperWidth: modelData.width // Some reasonable init value, to be updated
property int wallpaperHeight: modelData.height // Some reasonable init value, to be updated
property real movableXSpace: (effectiveWallpaperScale - 1) / 2 * screen.width
property real movableYSpace: (effectiveWallpaperScale - 1) / 2 * screen.height
// Position
property real clockX: (modelData.width / 2) + ((Math.random() < 0.5 ? -1 : 1) * modelData.width)
property real clockY: (modelData.height / 2) + ((Math.random() < 0.5 ? -1 : 1) * modelData.height)
property var textHorizontalAlignment: clockX < screen.width / 3 ? Text.AlignLeft :
(clockX > screen.width * 2 / 3 ? Text.AlignRight : Text.AlignHCenter)
// Colors
property color dominantColor: Appearance.colors.colPrimary
property bool dominantColorIsDark: dominantColor.hslLightness < 0.5
property color colText: ColorUtils.colorWithLightness(Appearance.colors.colPrimary, (dominantColorIsDark ? 0.8 : 0.12))
// Layer props
screen: modelData
exclusionMode: ExclusionMode.Ignore
WlrLayershell.layer: WlrLayer.Bottom
WlrLayershell.namespace: "quickshell:background"
anchors {
top: true
bottom: true
left: true
right: true
}
color: "transparent"
onWallpaperPathChanged: {
bgRoot.updateZoomScale()
// Clock position gets updated after zoom scale is updated
}
// Wallpaper zoom scale
function updateZoomScale() {
getWallpaperSizeProc.path = bgRoot.wallpaperPath
getWallpaperSizeProc.running = true;
}
Process {
id: getWallpaperSizeProc
property string path: bgRoot.wallpaperPath
command: [ "magick", "identify", "-format", "%w %h", path ]
stdout: StdioCollector {
id: wallpaperSizeOutputCollector
onStreamFinished: {
const output = wallpaperSizeOutputCollector.text
const [width, height] = output.split(" ").map(Number);
bgRoot.wallpaperWidth = width
bgRoot.wallpaperHeight = height
bgRoot.effectiveWallpaperScale = Math.max(1, Math.min(
bgRoot.preferredWallpaperScale,
width / bgRoot.screen.width,
height / bgRoot.screen.height
));
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 = clock.implicitWidth
leastBusyRegionProc.contentHeight = clock.implicitHeight
leastBusyRegionProc.horizontalPadding = (effectiveWallpaperScale - 1) / 2 * screen.width + 100
leastBusyRegionProc.verticalPadding = (effectiveWallpaperScale - 1) / 2 * screen.height + 100
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.configPath("scripts/images/least_busy_region.py"),
"--screen-width", bgRoot.screen.width,
"--screen-height", bgRoot.screen.height,
"--width", contentWidth,
"--height", contentHeight,
"--horizontal-padding", horizontalPadding,
"--vertical-padding", verticalPadding,
path
]
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.clockY = parsedContent.center_y
bgRoot.dominantColor = parsedContent.dominant_color || Appearance.colors.colPrimary
}
}
}
// Wallpaper
Image {
property real value // 0 to 1, for offset
value: {
// Range = half-groups that workspaces span on
const chunkSize = 5;
const lower = Math.floor(bgRoot.firstWorkspaceId / chunkSize) * chunkSize;
const upper = Math.ceil(bgRoot.lastWorkspaceId / chunkSize) * chunkSize;
const range = upper - lower;
return (bgRoot.monitor.activeWorkspace.id - lower) / range
+ (0.15 * GlobalStates.sidebarRightOpen * Config.options.background.parallax.enableSidebar)
- (0.15 * GlobalStates.sidebarLeftOpen * Config.options.background.parallax.enableSidebar)
}
property real effectiveValue: Math.max(0, Math.min(1, value))
x: -(bgRoot.movableXSpace) - (effectiveValue - 0.5) * 2 * bgRoot.movableXSpace
y: -(bgRoot.movableYSpace)
source: bgRoot.wallpaperPath
fillMode: Image.PreserveAspectCrop
Behavior on x {
NumberAnimation {
duration: 600
easing.type: Easing.OutCubic
}
}
sourceSize {
width: bgRoot.screen.width * bgRoot.effectiveWallpaperScale
height: bgRoot.screen.height * bgRoot.effectiveWallpaperScale
}
// The clock
Item {
id: clock
anchors {
left: parent.left
top: parent.top
leftMargin: ((root.fixedClockPosition ? root.fixedClockX : bgRoot.clockX * bgRoot.effectiveWallpaperScale) - implicitWidth / 2)
topMargin: ((root.fixedClockPosition ? root.fixedClockY : bgRoot.clockY * bgRoot.effectiveWallpaperScale) - implicitHeight / 2)
Behavior on leftMargin {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
Behavior on topMargin {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
}
implicitWidth: clockColumn.implicitWidth
implicitHeight: clockColumn.implicitHeight
ColumnLayout {
id: clockColumn
anchors.centerIn: parent
spacing: -5
StyledText {
Layout.fillWidth: true
horizontalAlignment: bgRoot.textHorizontalAlignment
font {
family: Appearance.font.family.expressive
pixelSize: 85
weight: Font.Medium
}
color: bgRoot.colText
style: Text.Raised
styleColor: Appearance.colors.colShadow
text: DateTime.time
}
StyledText {
Layout.fillWidth: true
horizontalAlignment: bgRoot.textHorizontalAlignment
font {
family: Appearance.font.family.expressive
pixelSize: 20
weight: Font.Medium
}
color: bgRoot.colText
style: Text.Raised
styleColor: Appearance.colors.colShadow
text: DateTime.date
}
}
}
}
}
}
}
@@ -1,139 +0,0 @@
import "root:/"
import "root:/modules/common"
import "root:/modules/common/widgets"
import "root:/services"
import "root:/modules/common/functions/color_utils.js" as ColorUtils
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Hyprland
import Quickshell.Services.UPower
Scope {
id: root
property string filePath: `${Directories.state}/user/generated/wallpaper/least_busy_region.json`
property real defaultX: (Config.options?.background.clockX ?? -500)
property real defaultY: (Config.options?.background.clockY ?? -500)
property real centerX: defaultX
property real centerY: defaultY
property real effectiveCenterX: Config.options?.background.fixedClockPosition ? defaultX : centerX
property real effectiveCenterY: Config.options?.background.fixedClockPosition ? defaultY : centerY
property color dominantColor: Appearance.colors.colPrimary
property bool dominantColorIsDark: dominantColor.hslLightness < 0.5
property color colBackground: ColorUtils.transparentize(ColorUtils.mix(Appearance.colors.colPrimary, Appearance.colors.colSecondaryContainer), 1)
property color colText: ColorUtils.colorWithLightness(Appearance.colors.colPrimary, (root.dominantColorIsDark ? 0.8 : 0.12))
function updateWidgetPosition(fileContent) {
// console.log("[BackgroundWidgets] Updating widget position with content:", fileContent)
const parsedContent = JSON.parse(fileContent)
root.centerX = parsedContent.center_x
root.centerY = parsedContent.center_y
root.dominantColor = parsedContent.dominant_color || Appearance.colors.colPrimary
}
Timer {
id: delayedFileRead
interval: Config.options.hacks.arbitraryRaceConditionDelay
running: false
onTriggered: {
root.updateWidgetPosition(leastBusyRegionFileView.text())
}
}
FileView {
id: leastBusyRegionFileView
path: Qt.resolvedUrl(root.filePath)
watchChanges: !Config.options?.background.fixedClockPosition
onFileChanged: {
this.reload()
delayedFileRead.start()
}
onLoadedChanged: {
const fileContent = leastBusyRegionFileView.text()
root.updateWidgetPosition(fileContent)
}
}
Variants { // For each monitor
model: Quickshell.screens
LazyLoader {
required property var modelData
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(modelData)
activeAsync: !ToplevelManager.activeToplevel?.activated
component: PanelWindow { // Window
id: windowRoot
screen: modelData
property var textHorizontalAlignment: root.effectiveCenterX / monitor.scale < windowRoot.width / 3 ? Text.AlignLeft :
(root.effectiveCenterX / monitor.scale > windowRoot.width * 2 / 3 ? Text.AlignRight : Text.AlignHCenter)
WlrLayershell.layer: WlrLayer.Bottom
WlrLayershell.namespace: "quickshell:backgroundWidgets"
anchors {
top: true
bottom:true
left: true
right: true
}
color: "transparent"
// HyprlandWindow.visibleMask: Region { // Buggy with scaled monitors
// item: widgetBackground
// }
Rectangle {
id: widgetBackground
property real verticalPadding: 20
property real horizontalPadding: 30
radius: 40
color: root.colBackground
implicitHeight: columnLayout.implicitHeight + verticalPadding * 2
implicitWidth: columnLayout.implicitWidth + horizontalPadding * 2
anchors {
left: parent.left
top: parent.top
leftMargin: (root.effectiveCenterX / monitor.scale - implicitWidth / 2)
topMargin: (root.effectiveCenterY / monitor.scale - implicitHeight / 2)
Behavior on leftMargin {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
Behavior on topMargin {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
}
ColumnLayout {
id: columnLayout
anchors.centerIn: parent
spacing: -5
StyledText {
Layout.fillWidth: true
horizontalAlignment: windowRoot.textHorizontalAlignment
font.pixelSize: 95
color: root.colText
style: Text.Raised
styleColor: Appearance.colors.colShadow
text: DateTime.time
}
StyledText {
Layout.fillWidth: true
horizontalAlignment: windowRoot.textHorizontalAlignment
font.pixelSize: 25
color: root.colText
style: Text.Raised
styleColor: Appearance.colors.colShadow
text: DateTime.date
}
}
}
}
}
}
}
+1 -1
View File
@@ -3,7 +3,7 @@ import "root:/services"
import "root:/modules/common/"
import "root:/modules/common/widgets"
import "root:/modules/common/functions/color_utils.js" as ColorUtils
import "root:/modules/bar/weather"
import "./weather"
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
@@ -158,14 +158,20 @@ Scope { // Scope
spacing: 10
Behavior on implicitWidth {
id: contentWidthBehavior
enabled: false
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
Behavior on implicitHeight {
id: contentHeightBehavior
enabled: false
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
currentIndex: tabBar.externalTrackedTab
onCurrentIndexChanged: {
contentWidthBehavior.enabled = true;
contentHeightBehavior.enabled = true;
tabBar.enableIndicatorAnimation = true;
root.selectedTab = currentIndex;
}
@@ -168,6 +168,7 @@ Singleton {
property string iconNerd: "SpaceMono NF"
property string monospace: "JetBrains Mono NF"
property string reading: "Readex Pro"
property string expressive: "Space Grotesk"
}
property QtObject pixelSize: QtObject {
property int smallest: 10
@@ -98,6 +98,12 @@ Singleton {
property bool fixedClockPosition: false
property real clockX: -500
property real clockY: -500
property string wallpaperPath: Quickshell.configPath("assets/images/default_wallpaper.png")
property JsonObject parallax: JsonObject {
property bool enableWorkspace: true
property real workspaceZoom: 1.07 // Relative to your screen, not wallpaper size
property bool enableSidebar: true
}
}
property JsonObject bar: JsonObject {
+115 -118
View File
@@ -19,131 +19,128 @@ Scope { // Scope
Variants { // For each monitor
model: Quickshell.screens
LazyLoader {
id: dockLoader
PanelWindow { // Window
required property var modelData
activeAsync: Config.options?.dock.hoverToReveal || (!ToplevelManager.activeToplevel?.activated)
id: dockRoot
screen: modelData
property bool reveal: root.pinned
|| (Config.options?.dock.hoverToReveal && dockMouseArea.containsMouse)
|| dockApps.requestDockShow
|| (!ToplevelManager.activeToplevel?.activated)
component: PanelWindow { // Window
id: dockRoot
screen: dockLoader.modelData
property bool reveal: root.pinned
|| (Config.options?.dock.hoverToReveal && dockMouseArea.containsMouse)
|| dockApps.requestDockShow
|| (!ToplevelManager.activeToplevel?.activated)
anchors {
bottom: true
left: true
right: true
}
exclusiveZone: root.pinned ? implicitHeight
- (Appearance.sizes.hyprlandGapsOut)
- (Appearance.sizes.elevationMargin - Appearance.sizes.hyprlandGapsOut) : 0
implicitWidth: dockBackground.implicitWidth
WlrLayershell.namespace: "quickshell:dock"
color: "transparent"
implicitHeight: (Config.options?.dock.height ?? 70) + Appearance.sizes.elevationMargin + Appearance.sizes.hyprlandGapsOut
mask: Region {
item: dockMouseArea
}
MouseArea {
id: dockMouseArea
height: parent.height
anchors {
bottom: true
left: true
right: true
}
exclusiveZone: root.pinned ? implicitHeight
- (Appearance.sizes.hyprlandGapsOut)
- (Appearance.sizes.elevationMargin - Appearance.sizes.hyprlandGapsOut) : 0
implicitWidth: dockBackground.implicitWidth
WlrLayershell.namespace: "quickshell:dock"
color: "transparent"
implicitHeight: (Config.options?.dock.height ?? 70) + Appearance.sizes.elevationMargin + Appearance.sizes.hyprlandGapsOut
mask: Region {
item: dockMouseArea
}
MouseArea {
id: dockMouseArea
anchors.top: parent.top
height: parent.height
anchors.topMargin: dockRoot.reveal ? 0 :
top: parent.top
topMargin: dockRoot.reveal ? 0 :
Config.options?.dock.hoverToReveal ? (dockRoot.implicitHeight - Config.options.dock.hoverRegionHeight) :
(dockRoot.implicitHeight + 1)
anchors.left: parent.left
anchors.right: parent.right
hoverEnabled: true
Behavior on anchors.topMargin {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
Item {
id: dockHoverRegion
anchors.fill: parent
Item { // Wrapper for the dock background
id: dockBackground
anchors {
top: parent.top
bottom: parent.bottom
horizontalCenter: parent.horizontalCenter
}
implicitWidth: dockRow.implicitWidth + 5 * 2
height: parent.height - Appearance.sizes.elevationMargin - Appearance.sizes.hyprlandGapsOut
StyledRectangularShadow {
target: dockVisualBackground
}
Rectangle { // The real rectangle that is visible
id: dockVisualBackground
property real margin: Appearance.sizes.elevationMargin
anchors.fill: parent
anchors.topMargin: Appearance.sizes.elevationMargin
anchors.bottomMargin: Appearance.sizes.hyprlandGapsOut
color: Appearance.colors.colLayer0
border.width: 1
border.color: Appearance.m3colors.m3outlineVariant
radius: Appearance.rounding.large
}
RowLayout {
id: dockRow
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
spacing: 3
property real padding: 5
VerticalButtonGroup {
Layout.topMargin: Appearance.sizes.hyprlandGapsOut // why does this work
GroupButton { // Pin button
baseWidth: 35
baseHeight: 35
clickedWidth: baseWidth
clickedHeight: baseHeight + 20
buttonRadius: Appearance.rounding.normal
toggled: root.pinned
onClicked: root.pinned = !root.pinned
contentItem: MaterialSymbol {
text: "keep"
horizontalAlignment: Text.AlignHCenter
iconSize: Appearance.font.pixelSize.larger
color: root.pinned ? Appearance.m3colors.m3onPrimary : Appearance.colors.colOnLayer0
}
}
}
DockSeparator {}
DockApps { id: dockApps; }
DockSeparator {}
DockButton {
Layout.fillHeight: true
onClicked: Hyprland.dispatch("global quickshell:overviewToggle")
contentItem: MaterialSymbol {
anchors.fill: parent
horizontalAlignment: Text.AlignHCenter
font.pixelSize: parent.width / 2
text: "apps"
color: Appearance.colors.colOnLayer0
}
}
}
}
}
horizontalCenter: parent.horizontalCenter
}
implicitWidth: dockHoverRegion.implicitWidth + Appearance.sizes.elevationMargin * 2
hoverEnabled: true
Behavior on anchors.topMargin {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
Item {
id: dockHoverRegion
anchors.fill: parent
implicitWidth: dockBackground.implicitWidth
Item { // Wrapper for the dock background
id: dockBackground
anchors {
top: parent.top
bottom: parent.bottom
horizontalCenter: parent.horizontalCenter
}
implicitWidth: dockRow.implicitWidth + 5 * 2
height: parent.height - Appearance.sizes.elevationMargin - Appearance.sizes.hyprlandGapsOut
StyledRectangularShadow {
target: dockVisualBackground
}
Rectangle { // The real rectangle that is visible
id: dockVisualBackground
property real margin: Appearance.sizes.elevationMargin
anchors.fill: parent
anchors.topMargin: Appearance.sizes.elevationMargin
anchors.bottomMargin: Appearance.sizes.hyprlandGapsOut
color: Appearance.colors.colLayer0
border.width: 1
border.color: Appearance.m3colors.m3outlineVariant
radius: Appearance.rounding.large
}
RowLayout {
id: dockRow
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
spacing: 3
property real padding: 5
VerticalButtonGroup {
Layout.topMargin: Appearance.sizes.hyprlandGapsOut // why does this work
GroupButton { // Pin button
baseWidth: 35
baseHeight: 35
clickedWidth: baseWidth
clickedHeight: baseHeight + 20
buttonRadius: Appearance.rounding.normal
toggled: root.pinned
onClicked: root.pinned = !root.pinned
contentItem: MaterialSymbol {
text: "keep"
horizontalAlignment: Text.AlignHCenter
iconSize: Appearance.font.pixelSize.larger
color: root.pinned ? Appearance.m3colors.m3onPrimary : Appearance.colors.colOnLayer0
}
}
}
DockSeparator {}
DockApps { id: dockApps; }
DockSeparator {}
DockButton {
Layout.fillHeight: true
onClicked: Hyprland.dispatch("global quickshell:overviewToggle")
contentItem: MaterialSymbol {
anchors.fill: parent
horizontalAlignment: Text.AlignHCenter
font.pixelSize: parent.width / 2
text: "apps"
color: Appearance.colors.colOnLayer0
}
}
}
}
}
}
}
}
@@ -149,7 +149,7 @@ Item { // Player instance
Rectangle {
anchors.fill: parent
color: ColorUtils.transparentize(blendedColors.colLayer0, 0.25)
color: ColorUtils.transparentize(blendedColors.colLayer0, 0.3)
radius: root.popupRounding
}
}
@@ -37,11 +37,8 @@ Scope {
// item: GlobalStates.overviewOpen ? columnLayout : null
// }
anchors {
top: true
left: true
right: true
bottom: true
}
@@ -85,6 +82,7 @@ Scope {
function setSearchingText(text) {
searchWidget.setSearchingText(text);
searchWidget.focusFirstItemIfNeeded();
}
ColumnLayout {
@@ -396,6 +396,8 @@ Item { // Wrapper
}
}
onModelChanged: root.focusFirstItemIfNeeded()
delegate: SearchItem {
// The selectable item for each search result
required property var modelData
@@ -11,77 +11,56 @@ Scope {
id: screenCorners
readonly property Toplevel activeWindow: ToplevelManager.activeToplevel
component CornerPanelWindow: PanelWindow {
id: cornerPanelWindow
visible: (Config.options.appearance.fakeScreenRounding === 1 || (Config.options.appearance.fakeScreenRounding === 2 && !activeWindow?.fullscreen))
property var corner
exclusionMode: ExclusionMode.Ignore
mask: Region {
item: null
}
WlrLayershell.namespace: "quickshell:screenCorners"
WlrLayershell.layer: WlrLayer.Overlay
color: "transparent"
anchors {
top: cornerPanelWindow.corner === RoundCorner.CornerEnum.TopLeft || cornerPanelWindow.corner === RoundCorner.CornerEnum.TopRight
left: cornerPanelWindow.corner === RoundCorner.CornerEnum.TopLeft || cornerPanelWindow.corner === RoundCorner.CornerEnum.BottomLeft
bottom: cornerPanelWindow.corner === RoundCorner.CornerEnum.BottomLeft || cornerPanelWindow.corner === RoundCorner.CornerEnum.BottomRight
right: cornerPanelWindow.corner === RoundCorner.CornerEnum.TopRight || cornerPanelWindow.corner === RoundCorner.CornerEnum.BottomRight
}
implicitWidth: cornerWidget.implicitWidth
implicitHeight: cornerWidget.implicitHeight
RoundCorner {
id: cornerWidget
size: Appearance.rounding.screenRounding
corner: cornerPanelWindow.corner
}
}
Variants {
model: Quickshell.screens
PanelWindow {
visible: (Config.options.appearance.fakeScreenRounding === 1
|| (Config.options.appearance.fakeScreenRounding === 2
&& !activeWindow?.fullscreen))
property var modelData
screen: modelData
exclusionMode: ExclusionMode.Ignore
mask: Region {
item: null
}
// HyprlandWindow.visibleMask: Region {
// Region {
// item: topLeftCorner
// }
// Region {
// item: topRightCorner
// }
// Region {
// item: bottomLeftCorner
// }
// Region {
// item: bottomRightCorner
// }
// }
WlrLayershell.namespace: "quickshell:screenCorners"
WlrLayershell.layer: WlrLayer.Overlay
color: "transparent"
anchors {
top: true
left: true
right: true
bottom: true
}
RoundCorner {
id: topLeftCorner
anchors.top: parent.top
anchors.left: parent.left
size: Appearance.rounding.screenRounding
Scope {
required property var modelData
CornerPanelWindow {
screen: modelData
corner: RoundCorner.CornerEnum.TopLeft
}
RoundCorner {
id: topRightCorner
anchors.top: parent.top
anchors.right: parent.right
size: Appearance.rounding.screenRounding
CornerPanelWindow {
screen: modelData
corner: RoundCorner.CornerEnum.TopRight
}
RoundCorner {
id: bottomLeftCorner
anchors.bottom: parent.bottom
anchors.left: parent.left
size: Appearance.rounding.screenRounding
CornerPanelWindow {
screen: modelData
corner: RoundCorner.CornerEnum.BottomLeft
}
RoundCorner {
id: bottomRightCorner
anchors.bottom: parent.bottom
anchors.right: parent.right
size: Appearance.rounding.screenRounding
CornerPanelWindow {
screen: modelData
corner: RoundCorner.CornerEnum.BottomRight
}
}
}
}
@@ -78,7 +78,6 @@ Scope {
StyledText { // Small instruction
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
font.family: Appearance.font.family.title
font.pixelSize: Appearance.font.pixelSize.normal
text: Translation.tr("Arrow keys to navigate, Enter to select\nEsc or click anywhere to cancel")
}
@@ -438,7 +438,7 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\)
return {
name: `${messageInputField.text.trim().split(" ").length == 1 ? (root.commandPrefix + "save ") : ""}${chatName}`,
displayName: `${chatName}`,
description: Translation.tr("Save chat from %1").arg(chatName),
description: Translation.tr("Save chat to %1").arg(chatName),
}
})
} else if (messageInputField.text.startsWith(`${root.commandPrefix}load`)) {
@@ -8,6 +8,7 @@ CONFIG_DIR="$XDG_CONFIG_HOME/quickshell/$QUICKSHELL_CONFIG_NAME"
CACHE_DIR="$XDG_CACHE_HOME/quickshell"
STATE_DIR="$XDG_STATE_HOME/quickshell"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SHELL_CONFIG_FILE="$XDG_CONFIG_HOME/illogical-impulse/config.json"
MATUGEN_DIR="$XDG_CONFIG_HOME/matugen"
terminalscheme="$SCRIPT_DIR/terminal/scheme-base.json"
@@ -59,14 +60,14 @@ post_process() {
handle_kde_material_you_colors &
# Determine the largest region on the wallpaper that's sufficiently un-busy to put widgets in
if [ ! -f "$MATUGEN_DIR/scripts/least_busy_region.py" ]; then
echo "Error: least_busy_region.py script not found in $MATUGEN_DIR/scripts/"
else
"$MATUGEN_DIR/scripts/least_busy_region.py" \
--screen-width "$screen_width" --screen-height "$screen_height" \
--width 300 --height 200 \
"$wallpaper_path" > "$STATE_DIR"/user/generated/wallpaper/least_busy_region.json
fi
# if [ ! -f "$MATUGEN_DIR/scripts/least_busy_region.py" ]; then
# echo "Error: least_busy_region.py script not found in $MATUGEN_DIR/scripts/"
# else
# "$MATUGEN_DIR/scripts/least_busy_region.py" \
# --screen-width "$screen_width" --screen-height "$screen_height" \
# --width 300 --height 200 \
# "$wallpaper_path" > "$STATE_DIR"/user/generated/wallpaper/least_busy_region.json
# fi
}
check_and_prompt_upscale() {
@@ -227,10 +228,10 @@ switch() {
else
matugen_args=(image "$imgpath")
generate_colors_material_args=(--path "$imgpath")
# Set wallpaper with swww
swww img "$imgpath" --transition-step 100 --transition-fps 120 \
--transition-type grow --transition-angle 30 --transition-duration 1 \
--transition-pos "$cursorposx, $cursorposy_inverted" &
# Update wallpaper path in config
if [ -f "$SHELL_CONFIG_FILE" ]; then
jq --arg path "$imgpath" '.background.wallpaperPath = $path' "$SHELL_CONFIG_FILE" > "$SHELL_CONFIG_FILE.tmp" && mv "$SHELL_CONFIG_FILE.tmp" "$SHELL_CONFIG_FILE"
fi
remove_restore
fi
fi
@@ -318,7 +319,7 @@ main() {
;;
--noswitch)
noswitch_flag="1"
imgpath=$(swww query | head -1 | awk -F 'image: ' '{print $2}')
imgpath=$(jq -r '.background.wallpaperPath' "$SHELL_CONFIG_FILE" 2>/dev/null || echo "")
shift
;;
*)
+341
View File
@@ -0,0 +1,341 @@
#!/usr/bin/env python3
# Disclaimer: This script was ai-generated and went through minimal revision.
import os
os.environ["OPENCV_LOG_LEVEL"] = "SILENT"
import cv2
import numpy as np
import argparse
import json
def center_crop(img, target_w, target_h):
h, w = img.shape[:2]
if w == target_w and h == target_h:
return img
x1 = max(0, (w - target_w) // 2)
y1 = max(0, (h - target_h) // 2)
x2 = x1 + target_w
y2 = y1 + target_h
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):
img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
if img is None:
raise FileNotFoundError(f"Image not found: {image_path}")
orig_h, orig_w = img.shape
scale = 1.0
if screen_width is not None and screen_height is not None:
scale_w = screen_width / orig_w
scale_h = screen_height / orig_h
if screen_mode == "fill":
scale = max(scale_w, scale_h)
else:
scale = min(scale_w, scale_h)
new_w = int(orig_w * scale)
new_h = int(orig_h * scale)
if verbose:
print(f"Scaling image from {orig_w}x{orig_h} to {new_w}x{new_h} (scale: {scale:.3f}, mode: {screen_mode})")
img = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_LANCZOS4)
img = center_crop(img, screen_width, screen_height)
if verbose:
print(f"Cropped image to {screen_width}x{screen_height}")
else:
if verbose:
print(f"Using original image size: {orig_w}x{orig_h}")
arr = img.astype(np.float64)
h, w = arr.shape
# Use OpenCV's integral for fast computation
integral = cv2.integral(arr, sdepth=cv2.CV_64F)[1:,1:]
integral_sq = cv2.integral(arr**2, sdepth=cv2.CV_64F)[1:,1:]
def region_sum(ii, x1, y1, x2, y2):
total = ii[y2, x2]
if x1 > 0:
total -= ii[y2, x1-1]
if y1 > 0:
total -= ii[y1-1, x2]
if x1 > 0 and y1 > 0:
total += ii[y1-1, x1-1]
return total
min_var = None
min_coords = (0, 0)
area = region_width * region_height
x_start = horizontal_padding
y_start = vertical_padding
x_end = w - region_width - horizontal_padding + 1
y_end = h - region_height - vertical_padding + 1
for y in range(y_start, max(y_end, y_start+1), stride):
for x in range(x_start, max(x_end, x_start+1), stride):
x1, y1 = x, y
x2, y2 = x + region_width - 1, y + region_height - 1
s = region_sum(integral, x1, y1, x2, y2)
s2 = region_sum(integral_sq, x1, y1, x2, y2)
mean = s / area
var = (s2 / area) - (mean ** 2)
if (min_var is None) or (var < min_var):
min_var = var
min_coords = (x, y)
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):
img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
if img is None:
raise FileNotFoundError(f"Image not found: {image_path}")
orig_h, orig_w = img.shape
scale = 1.0
if screen_width is not None and screen_height is not None:
scale_w = screen_width / orig_w
scale_h = screen_height / orig_h
if screen_mode == "fill":
scale = max(scale_w, scale_h)
else:
scale = min(scale_w, scale_h)
new_w = int(orig_w * scale)
new_h = int(orig_h * scale)
if verbose:
print(f"Scaling image from {orig_w}x{orig_h} to {new_w}x{new_h} (scale: {scale:.3f}, mode: {screen_mode})")
img = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_LANCZOS4)
img = center_crop(img, screen_width, screen_height)
if verbose:
print(f"Cropped image to {screen_width}x{screen_height}")
else:
if verbose:
print(f"Using original image size: {orig_w}x{orig_h}")
arr = img.astype(np.float64)
h, w = arr.shape
# Use OpenCV's integral for fast computation
integral = cv2.integral(arr, sdepth=cv2.CV_64F)[1:,1:]
integral_sq = cv2.integral(arr**2, sdepth=cv2.CV_64F)[1:,1:]
def region_sum(ii, x1, y1, x2, y2):
total = ii[y2, x2]
if x1 > 0:
total -= ii[y2, x1-1]
if y1 > 0:
total -= ii[y1-1, x2]
if x1 > 0 and y1 > 0:
total += ii[y1-1, x1-1]
return total
min_size = 10
max_size = min(h, int(w / aspect_ratio)) if aspect_ratio >= 1.0 else min(int(h * aspect_ratio), w)
best = None
best_size = min_size
while min_size <= max_size:
mid = (min_size + max_size) // 2
if aspect_ratio >= 1.0:
region_h = mid
region_w = int(mid * aspect_ratio)
else:
region_w = mid
region_h = int(mid / aspect_ratio)
if region_w > w or region_h > h:
max_size = mid - 1
continue
found = False
x_start = horizontal_padding
y_start = vertical_padding
x_end = w - region_w - horizontal_padding + 1
y_end = h - region_h - vertical_padding + 1
for y in range(y_start, max(y_end, y_start+1), stride):
for x in range(x_start, max(x_end, x_start+1), stride):
x1, y1 = x, y
x2, y2 = x + region_w - 1, y + region_h - 1
s = region_sum(integral, x1, y1, x2, y2)
s2 = region_sum(integral_sq, x1, y1, x2, y2)
area = region_w * region_h
mean = s / area
var = (s2 / area) - (mean ** 2)
if var <= threshold:
found = True
best = (x, y, region_w, region_h, var)
break
if found:
break
if found:
best_size = mid
min_size = mid + 1
else:
max_size = mid - 1
if best:
x, y, region_w, region_h, var = best
center_x = x + region_w // 2
center_y = y + region_h // 2
return (center_x, center_y), (region_w, region_h), var
else:
return None, (0, 0), None
def draw_region(image_path, coords, region_width=300, region_height=200, output_path='output.png', screen_width=None, screen_height=None, screen_mode="fill"):
img = cv2.imread(image_path)
if img is None:
raise FileNotFoundError(f"Image not found: {image_path}")
orig_h, orig_w = img.shape[:2]
if screen_width is not None and screen_height is not None:
scale_w = screen_width / orig_w
scale_h = screen_height / orig_h
if screen_mode == "fill":
scale = max(scale_w, scale_h)
else:
scale = min(scale_w, scale_h)
new_w = int(orig_w * scale)
new_h = int(orig_h * scale)
img = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_LANCZOS4)
img = center_crop(img, screen_width, screen_height)
x, y = coords
cv2.rectangle(img, (x, y), (x+region_width-1, y+region_height-1), (0,0,255), 3)
cv2.imwrite(output_path, img)
print(f"Saved output image with rectangle at {output_path}")
def draw_largest_region(image_path, center, size, output_path='output.png', screen_width=None, screen_height=None, screen_mode="fill"):
img = cv2.imread(image_path)
if img is None:
raise FileNotFoundError(f"Image not found: {image_path}")
orig_h, orig_w = img.shape[:2]
if screen_width is not None and screen_height is not None:
scale_w = screen_width / orig_w
scale_h = screen_height / orig_h
if screen_mode == "fill":
scale = max(scale_w, scale_h)
else:
scale = min(scale_w, scale_h)
new_w = int(orig_w * scale)
new_h = int(orig_h * scale)
img = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_LANCZOS4)
img = center_crop(img, screen_width, screen_height)
cx, cy = center
region_w, region_h = size
x1 = cx - region_w // 2
y1 = cy - region_h // 2
x2 = cx + region_w // 2 - 1
y2 = cy + region_h // 2 - 1
cv2.rectangle(img, (x1, y1), (x2, y2), (255,0,0), 3)
cv2.imwrite(output_path, img)
print(f"Saved output image with largest region at {output_path}")
def get_dominant_color(image_path, x, y, w, h, screen_width=None, screen_height=None, screen_mode="fill"):
img = cv2.imread(image_path)
if img is None:
raise FileNotFoundError(f"Image not found: {image_path}")
orig_h, orig_w = img.shape[:2]
if screen_width is not None and screen_height is not None:
scale_w = screen_width / orig_w
scale_h = screen_height / orig_h
if screen_mode == "fill":
scale = max(scale_w, scale_h)
else:
scale = min(scale_w, scale_h)
new_w = int(orig_w * scale)
new_h = int(orig_h * scale)
img = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_LANCZOS4)
img = center_crop(img, screen_width, screen_height)
# Ensure region is within bounds
x = max(0, x)
y = max(0, y)
w = max(1, min(w, img.shape[1] - x))
h = max(1, min(h, img.shape[0] - y))
region = img[y:y+h, x:x+w]
if region.size == 0 or region.shape[0] == 0 or region.shape[1] == 0:
return [0, 0, 0]
region = region.reshape((-1, 3))
# Filter out black pixels (optional, improves accuracy for some images)
non_black = region[np.any(region > 10, axis=1)]
if non_black.shape[0] == 0:
non_black = region
region = np.float32(non_black)
if region.shape[0] < 3:
return [int(x) for x in np.mean(region, axis=0)]
# K-means to find dominant color
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0)
K = min(3, region.shape[0])
_, labels, centers = cv2.kmeans(region, K, None, criteria, 10, cv2.KMEANS_RANDOM_CENTERS)
counts = np.bincount(labels.flatten())
dominant = centers[np.argmax(counts)]
return [int(x) for x in dominant]
def main():
parser = argparse.ArgumentParser(description="Find least busy region in an image and output a JSON. Made for determining a suitable position for a wallpaper widget.")
parser.add_argument("image_path", help="Path to the input image")
parser.add_argument("--width", type=int, default=300, help="Region width")
parser.add_argument("--height", type=int, default=200, help="Region height")
parser.add_argument("-v", "--visual-output", action="store_true", help="Output image with rectangle")
parser.add_argument("--screen-width", type=int, default=1920, help="Screen width for wallpaper scaling")
parser.add_argument("--screen-height", type=int, default=1080, help="Screen height for wallpaper scaling")
parser.add_argument("--stride", type=int, default=10, help="Step size for sliding window (higher is faster, less precise)")
parser.add_argument("--screen-mode", choices=["fill", "fit"], default="fill", help="Wallpaper scaling mode: 'fill' (default) or 'fit'")
parser.add_argument("--verbose", action="store_true", help="Print verbose output")
parser.add_argument("-l", "--largest-region", action="store_true", help="Find the largest region under the variance threshold and output its center")
parser.add_argument("-t", "--variance-threshold", type=float, default=1000.0, help="Variance threshold 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("--vertical-padding", "-vp", type=int, default=50, help="Minimum vertical distance from region to image edge")
args = parser.parse_args()
if args.largest_region:
center, size, var = find_largest_region(
args.image_path,
screen_width=args.screen_width,
screen_height=args.screen_height,
verbose=args.verbose,
stride=args.stride,
screen_mode=args.screen_mode,
threshold=args.variance_threshold,
aspect_ratio=args.aspect_ratio,
horizontal_padding=args.horizontal_padding,
vertical_padding=args.vertical_padding
)
if center:
if args.visual_output:
draw_largest_region(args.image_path, center, size, screen_width=args.screen_width, screen_height=args.screen_height, screen_mode=args.screen_mode)
# Extract dominant color
cx, cy = center
region_w, region_h = size
x1 = cx - region_w // 2
y1 = cy - region_h // 2
dominant_color = get_dominant_color(
args.image_path, x1, y1, region_w, region_h,
screen_width=args.screen_width, screen_height=args.screen_height, screen_mode=args.screen_mode
)
dominant_color_hex = '#{:02x}{:02x}{:02x}'.format(*dominant_color)
print(json.dumps({
"center_x": center[0],
"center_y": center[1],
"width": size[0],
"height": size[1],
"variance": var,
"dominant_color": dominant_color_hex
}))
else:
print(json.dumps({"error": "No region found under the threshold."}))
return
coords, variance = find_least_busy_region(
args.image_path,
region_width=args.width,
region_height=args.height,
screen_width=args.screen_width,
screen_height=args.screen_height,
verbose=args.verbose,
stride=args.stride,
screen_mode=args.screen_mode,
horizontal_padding=args.horizontal_padding,
vertical_padding=args.vertical_padding
)
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)
# Output JSON with center point
center_x = coords[0] + args.width // 2
center_y = coords[1] + args.height // 2
dominant_color = get_dominant_color(
args.image_path, coords[0], coords[1], args.width, args.height,
screen_width=args.screen_width, screen_height=args.screen_height, screen_mode=args.screen_mode
)
dominant_color_hex = '#{:02x}{:02x}{:02x}'.format(*dominant_color)
print(json.dumps({
"center_x": center_x,
"center_y": center_y,
"width": args.width,
"height": args.height,
"variance": variance,
"dominant_color": dominant_color_hex
}))
if __name__ == "__main__":
main()
@@ -27,7 +27,7 @@ Singleton {
}
function handleFirstRun() {
Quickshell.execDetached(["bash", "-c", `swww query | grep 'image' || '${Directories.wallpaperSwitchScriptPath}' '${root.defaultWallpaperPath}'`])
Quickshell.execDetached([Directories.wallpaperSwitchScriptPath, root.defaultWallpaperPath])
Quickshell.execDetached(["bash", "-c", `qs -p '${root.welcomeQmlPath}'`])
}
@@ -41,7 +41,7 @@ Singleton {
Process {
id: getDefaultKeybinds
running: true
command: [root.keybindParserPath, "--path", root.defaultKeybindConfigPath,]
command: [root.keybindParserPath, "--path", root.defaultKeybindConfigPath]
stdout: SplitParser {
onRead: data => {
@@ -36,9 +36,9 @@ Singleton {
const nameMatch = textOsRelease.match(/^NAME="(.+?)"/m)
distroName = prettyNameMatch ? prettyNameMatch[1] : (nameMatch ? nameMatch[1].replace(/Linux/i, "").trim() : "Unknown")
// Extract the ID (LOGO field, fallback to "unknown")
const logoMatch = textOsRelease.match(/^LOGO=(.+)$/m)
distroId = logoMatch ? logoMatch[1].replace(/"/g, "") : "unknown"
// Extract the ID
const idMatch = textOsRelease.match(/^ID="?(.+?)"?$/m)
distroId = idMatch ? idMatch[1] : "unknown"
// Extract additional URLs and logo
const homeUrlMatch = textOsRelease.match(/^HOME_URL="(.+?)"/m)
+3 -3
View File
@@ -6,7 +6,7 @@
//@ pragma Env QT_SCALE_FACTOR=1
import "./modules/common/"
import "./modules/backgroundWidgets/"
import "./modules/background/"
import "./modules/bar/"
import "./modules/cheatsheet/"
import "./modules/dock/"
@@ -30,7 +30,7 @@ ShellRoot {
// Enable/disable modules here. False = not loaded at all, so rest assured
// no unnecessary stuff will take up memory if you decide to only use, say, the overview.
property bool enableBar: true
property bool enableBackgroundWidgets: true
property bool enableBackground: true
property bool enableCheatsheet: true
property bool enableDock: true
property bool enableMediaControls: true
@@ -53,7 +53,7 @@ ShellRoot {
}
LazyLoader { active: enableBar; component: Bar {} }
LazyLoader { active: enableBackgroundWidgets; component: BackgroundWidgets {} }
LazyLoader { active: enableBackground; component: Background {} }
LazyLoader { active: enableCheatsheet; component: Cheatsheet {} }
LazyLoader { active: enableDock && Config.options.dock.enable; component: Dock {} }
LazyLoader { active: enableMediaControls; component: MediaControls {} }