1 Commits

75 changed files with 2858 additions and 2020 deletions
-2
View File
@@ -13,5 +13,3 @@ uv.lock
.qtcreator/ .qtcreator/
dist/ dist/
**/target/ **/target/
**/test-plugins/
**/Charts/
-5
View File
@@ -59,8 +59,6 @@ JsonObject {
} }
property int rounding: 8 property int rounding: 8
property int smoothing: 32 property int smoothing: 32
property Tray tray: Tray {
}
component Popouts: JsonObject { component Popouts: JsonObject {
property bool activeWindow: true property bool activeWindow: true
@@ -71,7 +69,4 @@ JsonObject {
property bool tray: true property bool tray: true
property bool upower: true property bool upower: true
} }
component Tray: JsonObject {
property int trayIconSize: 24
}
} }
+12 -9
View File
@@ -22,6 +22,7 @@ Singleton {
property alias notifs: adapter.notifs property alias notifs: adapter.notifs
property alias osd: adapter.osd property alias osd: adapter.osd
property alias overview: adapter.overview property alias overview: adapter.overview
property alias plugins: adapter.plugins
property bool recentlySaved: false property bool recentlySaved: false
property alias screenshot: adapter.screenshot property alias screenshot: adapter.screenshot
property alias services: adapter.services property alias services: adapter.services
@@ -100,9 +101,6 @@ Singleton {
border: barConfig.border, border: barConfig.border,
smoothing: barConfig.smoothing, smoothing: barConfig.smoothing,
height: barConfig.height, height: barConfig.height,
tray: {
trayIconSize: barConfig.tray.trayIconSize
},
popouts: { popouts: {
tray: barConfig.popouts.tray, tray: barConfig.popouts.tray,
audio: barConfig.popouts.audio, audio: barConfig.popouts.audio,
@@ -143,7 +141,8 @@ Singleton {
launcher: serializeLauncher(), launcher: serializeLauncher(),
colors: serializeColors(), colors: serializeColors(),
dock: serializeDock(), dock: serializeDock(),
screenshot: serializeScreenshot() screenshot: serializeScreenshot(),
plugins: serializePlugins()
}; };
} }
@@ -217,10 +216,6 @@ Singleton {
}, },
idle: { idle: {
timeouts: general.idle.timeouts timeouts: general.idle.timeouts
},
battery: {
popupThresholds: general.battery.popupThresholds,
critPerc: general.battery.critPerc
} }
}; };
} }
@@ -296,6 +291,13 @@ Singleton {
}; };
} }
function serializePlugins(): var {
return {
enabled: plugins.enabled,
entries: plugins.entries
};
}
function serializeScreenshot(): var { function serializeScreenshot(): var {
return { return {
enable_pp: screenshot.enable_pp, enable_pp: screenshot.enable_pp,
@@ -304,7 +306,6 @@ Singleton {
drop_shadow: screenshot.drop_shadow, drop_shadow: screenshot.drop_shadow,
rounded_corners: screenshot.rounded_corners, rounded_corners: screenshot.rounded_corners,
shadow_blur_radius: screenshot.shadow_blur_radius, shadow_blur_radius: screenshot.shadow_blur_radius,
shadow_blur_passes: screenshot.shadow_blur_passes,
shadow_color: screenshot.shadow_color, shadow_color: screenshot.shadow_color,
shadow_offset_x: screenshot.shadow_offset_x, shadow_offset_x: screenshot.shadow_offset_x,
shadow_offset_y: screenshot.shadow_offset_y shadow_offset_y: screenshot.shadow_offset_y
@@ -466,6 +467,8 @@ Singleton {
} }
property Overview overview: Overview { property Overview overview: Overview {
} }
property PluginConfig plugins: PluginConfig {
}
property Screenshot screenshot: Screenshot { property Screenshot screenshot: Screenshot {
} }
property Services services: Services { property Services services: Services {
-13
View File
@@ -4,8 +4,6 @@ import Quickshell
JsonObject { JsonObject {
property Apps apps: Apps { property Apps apps: Apps {
} }
property Battery battery: Battery {
}
property Color color: Color { property Color color: Color {
} }
property string dateFormat: "ddd d MMM - hh:mm:ss" property string dateFormat: "ddd d MMM - hh:mm:ss"
@@ -21,17 +19,6 @@ JsonObject {
property list<string> playback: ["mpv"] property list<string> playback: ["mpv"]
property list<string> terminal: ["kitty"] property list<string> terminal: ["kitty"]
} }
component Battery: JsonObject {
property int critPerc: 5
property list<var> popupThresholds: [
{
perc: 20,
name: qsTr("Low battery"),
message: qsTr("Battery is low"),
icon: "battery_android_frame_2"
},
]
}
component Color: JsonObject { component Color: JsonObject {
property int hyprsunsetTemp: 5000 property int hyprsunsetTemp: 5000
property string mode: "dark" property string mode: "dark"
+11
View File
@@ -0,0 +1,11 @@
import Quickshell.Io
JsonObject {
property bool enabled: false
property list<var> entries: [
{
id: "Plugin",
enabled: false
},
]
}
-1
View File
@@ -6,7 +6,6 @@ JsonObject {
property bool enable_pp: true property bool enable_pp: true
property string mode: "manual" property string mode: "manual"
property bool rounded_corners: false property bool rounded_corners: false
property int shadow_blur_passes: 1
property real shadow_blur_radius: 22.0 property real shadow_blur_radius: 22.0
property list<int> shadow_color: [0, 0, 0, 160] property list<int> shadow_color: [0, 0, 0, 160]
property real shadow_offset_x: 5.0 property real shadow_offset_x: 5.0
-47
View File
@@ -1,47 +0,0 @@
import Quickshell
import Quickshell.Services.UPower
import QtQuick
import ZShell
import qs.Config
import qs.Components.Toast
Scope {
id: root
readonly property real currentPerc: UPower.displayDevice.percentage
readonly property list<var> popupThresholds: [...Config.general.battery.popupThresholds].sort((a, b) => b.perc - a.perc)
Connections {
function onOnBatteryChanged(): void {
if (UPower.onBattery) {
if (Config.utilities.toasts.chargingChanged)
Toaster.toast(qsTr("Charger unplugged"), qsTr("Battery is discharging"), "power_off");
} else {
if (Config.utilities.toasts.chargingChanged)
Toaster.toast(qsTr("Charger plugged in"), qsTr("Battery is charging"), "power");
for (const level of root.popupThresholds)
level.warned = false;
}
}
target: UPower
}
Connections {
function onPercentageChanged(): void {
if (!UPower.onBattery)
return;
const p = UPower.displayDevice.percentage * 100;
for (const perc of root.popupThresholds) {
if (p <= perc.perc && !perc.warned) {
perc.warned = true;
console.log(perc.warned + "\n" + [...Config.general.battery.popupThresholds][0].warned);
Toaster.toast(perc.title ?? qsTr("Battery warning"), perc.message ?? qsTr("Battery perc is low"), perc.icon ?? "battery_android_alert", perc.critical ? Toast.Error : Toast.Warning);
}
}
}
target: UPower.displayDevice
}
}
+57 -32
View File
@@ -179,8 +179,6 @@ Singleton {
property string appIcon property string appIcon
property string appName property string appName
property string body property string body
property string cachedImageSource: ""
property bool cachingImage: false
property bool closed property bool closed
readonly property Connections conn: Connections { readonly property Connections conn: Connections {
function onActionsChanged(): void { function onActionsChanged(): void {
@@ -216,9 +214,9 @@ Singleton {
} }
function onImageChanged(): void { function onImageChanged(): void {
notif.imageSource = notif.notification.image || ""; notif.image = notif.notification.image;
notif.image = notif.imageSource; if (notif.notification?.image)
notif.cacheImageIfNeeded(); notif.dummyImageLoader.active = true;
} }
function onResidentChanged(): void { function onResidentChanged(): void {
@@ -235,12 +233,60 @@ Singleton {
target: notif.notification target: notif.notification
} }
readonly property LazyLoader dummyImageLoader: LazyLoader {
active: false
PanelWindow {
color: "transparent"
implicitHeight: Config.notifs.sizes.image
implicitWidth: Config.notifs.sizes.image
mask: Region {
}
Image {
function tryCache(): void {
if (status !== Image.Ready || width != Config.notifs.sizes.image || height != Config.notifs.sizes.image)
return;
const cacheKey = notif.appName + notif.summary + notif.id;
let h1 = 0xdeadbeef, h2 = 0x41c6ce57, ch;
for (let i = 0; i < cacheKey.length; i++) {
ch = cacheKey.charCodeAt(i);
h1 = Math.imul(h1 ^ ch, 2654435761);
h2 = Math.imul(h2 ^ ch, 1597334677);
}
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
const hash = (h2 >>> 0).toString(16).padStart(8, 0) + (h1 >>> 0).toString(16).padStart(8, 0);
const cache = `${Paths.notifimagecache}/${hash}.png`;
ZShellIo.saveItem(this, Qt.resolvedUrl(cache), () => {
notif.image = cache;
notif.dummyImageLoader.active = false;
});
}
anchors.fill: parent
asynchronous: true
cache: false
fillMode: Image.PreserveAspectCrop
opacity: 0
source: Qt.resolvedUrl(notif.image)
onHeightChanged: tryCache()
onStatusChanged: tryCache()
onWidthChanged: tryCache()
}
}
}
property real expireTimeout: 5 property real expireTimeout: 5
property bool hasActionIcons property bool hasActionIcons
property string id
property string image property string image
property string imageSource
property var locks: new Set() property var locks: new Set()
property string notifId
property Notification notification property Notification notification
property bool popup property bool popup
property bool resident property bool resident
@@ -283,26 +329,6 @@ Singleton {
} }
property int urgency: NotificationUrgency.Normal property int urgency: NotificationUrgency.Normal
function cacheImageIfNeeded(): void {
const source = imageSource;
if (!source || cachingImage)
return;
if (cachedImageSource === source)
return;
cachingImage = true;
ZShellIo.cacheImage(Qt.resolvedUrl(source), Paths.notifimagecache, (path, url) => {
cachedImageSource = source;
image = path;
cachingImage = false;
}, () => {
cachingImage = false;
});
}
function close(): void { function close(): void {
closed = true; closed = true;
if (locks.size === 0 && root.list.includes(this)) { if (locks.size === 0 && root.list.includes(this)) {
@@ -326,13 +352,14 @@ Singleton {
if (!notification) if (!notification)
return; return;
notifId = notification.id; id = notification.id;
summary = notification.summary; summary = notification.summary;
body = notification.body; body = notification.body;
appIcon = notification.appIcon; appIcon = notification.appIcon;
appName = notification.appName; appName = notification.appName;
imageSource = notification.image || ""; image = notification.image;
image = imageSource; if (notification?.image)
dummyImageLoader.active = true;
expireTimeout = notification.expireTimeout; expireTimeout = notification.expireTimeout;
urgency = notification.urgency; urgency = notification.urgency;
resident = notification.resident; resident = notification.resident;
@@ -342,8 +369,6 @@ Singleton {
text: a.text, text: a.text,
invoke: () => a.invoke() invoke: () => a.invoke()
})); }));
cacheImageIfNeeded();
} }
} }
} }
+107
View File
@@ -0,0 +1,107 @@
import Quickshell
import QtQuick
import QtQuick.Shapes
import qs.Components
import qs.Config
import qs.Modules as Modules
import qs.Modules.Notifications as Notifications
import qs.Modules.Notifications.Sidebar as Sidebar
import qs.Modules.Notifications.Sidebar.Utils as Utils
import qs.Modules.Dashboard as Dashboard
import qs.Modules.Osd as Osd
import qs.Modules.Launcher as Launcher
import qs.Modules.Resources as Resources
import qs.Modules.Drawing as Drawing
import qs.Modules.Settings as Settings
import qs.Modules.Dock as Dock
Shape {
id: root
required property Item bar
required property Panels panels
required property PersistentProperties visibilities
anchors.fill: parent
anchors.margins: Config.barConfig.border
anchors.topMargin: bar.implicitHeight
asynchronous: true
preferredRendererType: Shape.CurveRenderer
Drawing.Background {
startX: 0
startY: wrapper.y - rounding
wrapper: root.panels.drawing
}
Resources.Background {
startX: 0 - rounding
startY: 0
wrapper: root.panels.resources
}
Osd.Background {
startX: root.width - root.panels.sidebar.width
startY: (root.height - wrapper.height) / 2 - rounding
wrapper: root.panels.osd
}
Modules.Background {
invertBottomRounding: wrapper.x <= 0
rounding: root.panels.popouts.currentName.startsWith("updates") || root.panels.popouts.currentName.startsWith("audio") ? Appearance.rounding.normal : Appearance.rounding.smallest
startX: wrapper.x - rounding
startY: wrapper.y
wrapper: root.panels.popouts
}
Notifications.Background {
sidebar: sidebar
startX: root.width
startY: 0
wrapper: root.panels.notifications
}
Launcher.Background {
startX: (root.width - wrapper.width) / 2 - rounding
startY: root.height
wrapper: root.panels.launcher
}
Dashboard.Background {
startX: root.width - root.panels.dashboard.width - rounding
startY: 0
wrapper: root.panels.dashboard
}
Utils.Background {
sidebar: sidebar
startX: root.width
startY: root.height
wrapper: root.panels.utilities
}
Sidebar.Background {
id: sidebar
panels: root.panels
startX: root.width
startY: root.panels.notifications.height
wrapper: root.panels.sidebar
}
Settings.Background {
id: settings
startX: (root.width - wrapper.width) / 2 - rounding
startY: 0
wrapper: root.panels.settings
}
Dock.Background {
id: dock
startX: (root.width - wrapper.width) / 2 - rounding
startY: root.height
wrapper: root.panels.dock
}
}
-1
View File
@@ -23,7 +23,6 @@ Canvas {
ctx.save(); ctx.save();
ctx.lineWidth = root.penWidth; ctx.lineWidth = root.penWidth;
ctx.strokeStyle = root.penColor; ctx.strokeStyle = root.penColor;
ctx.lineJoin = "round";
ctx.lineCap = "round"; ctx.lineCap = "round";
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y); ctx.moveTo(points[0].x, points[0].y);
+3 -3
View File
@@ -22,20 +22,20 @@ CustomMouseArea {
} }
acceptedButtons: Qt.LeftButton | Qt.RightButton acceptedButtons: Qt.LeftButton | Qt.RightButton
enabled: z > 0 anchors.fill: root.visibilities.isDrawing ? parent : undefined
hoverEnabled: true hoverEnabled: true
visible: root.visibilities.isDrawing visible: root.visibilities.isDrawing
onPositionChanged: event => { onPositionChanged: event => {
const x = event.x; const x = event.x;
const y = event.y; const y = event.y;
if (root.visibilities.isDrawing && (event.buttons & Qt.LeftButton)) { if (root.visibilities.isDrawing && (event.buttons & Qt.LeftButton)) {
root.drawing.points.push(Qt.point(x, y)); root.drawing.points.push(Qt.point(x, y));
root.drawing.requestPaint(); root.drawing.requestPaint();
return;
} }
if (!(event.buttons & Qt.LeftButton) && root.inLeftPanel(root.popout, x, y)) { if (root.inLeftPanel(root.popout, x, y)) {
root.z = -2; root.z = -2;
root.panels.drawing.expanded = true; root.panels.drawing.expanded = true;
} }
+1 -8
View File
@@ -78,7 +78,7 @@ CustomMouseArea {
const dragY = y - dragStart.y; const dragY = y - dragStart.y;
if (root.visibilities.isDrawing && !root.inLeftPanel(root.panels.drawing, x, y)) { if (root.visibilities.isDrawing && !root.inLeftPanel(root.panels.drawing, x, y)) {
root.input.z = 2; // root.input.z = 2;
root.panels.drawing.expanded = false; root.panels.drawing.expanded = false;
} }
@@ -132,8 +132,6 @@ CustomMouseArea {
if (!inDashboardArea) { if (!inDashboardArea) {
root.dashboardShortcutActive = true; root.dashboardShortcutActive = true;
} }
if (root.panels.launcher.x + root.panels.launcher.width > root.panels.dashboardWrapper.x)
root.visibilities.launcher = false;
root.visibilities.settings = false; root.visibilities.settings = false;
root.visibilities.sidebar = false; root.visibilities.sidebar = false;
@@ -163,11 +161,6 @@ CustomMouseArea {
} }
if (root.visibilities.launcher) { if (root.visibilities.launcher) {
if (root.panels.dashboardWrapper.x < root.panels.launcher.x + root.panels.launcher.width) {
console.log("true");
root.visibilities.dashboard = false;
}
root.visibilities.dock = false; root.visibilities.dock = false;
root.visibilities.settings = false; root.visibilities.settings = false;
} }
+13 -26
View File
@@ -35,7 +35,7 @@ Variants {
property var root: Quickshell.shellDir property var root: Quickshell.shellDir
WlrLayershell.exclusionMode: ExclusionMode.Ignore WlrLayershell.exclusionMode: ExclusionMode.Ignore
// WlrLayershell.keyboardFocus: visibilities.dock || visibilities.launcher || visibilities.sidebar || visibilities.dashboard || visibilities.settings || visibilities.resources ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None WlrLayershell.keyboardFocus: visibilities.dock || visibilities.launcher || visibilities.sidebar || visibilities.dashboard || visibilities.settings || visibilities.resources ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
color: "transparent" color: "transparent"
contentItem.focus: true contentItem.focus: true
mask: visibilities.isDrawing ? null : region mask: visibilities.isDrawing ? null : region
@@ -229,7 +229,6 @@ Variants {
id: notifsBg id: notifsBg
panel: panels.notifications panel: panels.notifications
radius: Appearance.rounding.normal
} }
PanelBg { PanelBg {
@@ -305,34 +304,22 @@ Variants {
} }
} }
Loader { Drawing {
id: drawingLoader id: drawing
active: visibilities.isDrawing
anchors.fill: parent anchors.fill: parent
z: 2 z: 2
sourceComponent: Drawing {
id: drawing
}
} }
Loader { DrawingInput {
id: inputLoader id: input
active: visibilities.isDrawing bar: bar
anchors.fill: parent drawing: drawing
panels: panels
popout: panels.drawing
visibilities: visibilities
z: 2 z: 2
sourceComponent: DrawingInput {
id: input
bar: bar
drawing: drawingLoader.item
panels: panels
popout: panels.drawing
visibilities: visibilities
}
} }
Interactions { Interactions {
@@ -340,8 +327,8 @@ Variants {
anchors.fill: parent anchors.fill: parent
bar: bar bar: bar
drawing: drawingLoader.item drawing: drawing
input: inputLoader.item input: input
panels: panels panels: panels
popouts: panels.popouts popouts: panels.popouts
screen: scope.modelData screen: scope.modelData
@@ -352,7 +339,7 @@ Variants {
id: panels id: panels
bar: bar bar: bar
drawingItem: drawingLoader.item drawingItem: drawing
screen: scope.modelData screen: scope.modelData
visibilities: visibilities visibilities: visibilities
+18
View File
@@ -0,0 +1,18 @@
pragma Singleton
import Quickshell
import ZShell.Models
Singleton {
id: root
property alias plugins: plugins.entries
FileSystemModel {
id: plugins
nameFilters: ["*.qml"]
path: Quickshell.env("HOME") + "/.config/zshell"
recursive: false
}
}
+17
View File
@@ -0,0 +1,17 @@
import Quickshell
import QtQuick
import ZShell.Models
import qs.Config
Repeater {
model: FetchPlugins.plugins
LazyLoader {
required property FileSystemEntry modelData
activeAsync: Config.plugins.entries.some(p => {
return p.id === modelData.baseName && p.enabled;
})
source: modelData.path
}
}
+1 -1
View File
@@ -36,7 +36,7 @@ Singleton {
PersistentProperties { PersistentProperties {
id: props id: props
property bool enabled: Hypr.options.animations.enabled === 0 property bool enabled: Hypr.options["animations:enabled"] === 0
reloadableId: "gamemode" reloadableId: "gamemode"
} }
+1
View File
@@ -158,5 +158,6 @@ Singleton {
HyprExtras { HyprExtras {
id: extras id: extras
} }
} }
+3 -8
View File
@@ -30,17 +30,12 @@ MouseArea {
property real ey: screen.height property real ey: screen.height
required property LazyLoader loader required property LazyLoader loader
property bool onClient property bool onClient
property real realBorderWidth: onClient ? (Hypr.options.general.border_size ?? 1) : 2 property real realBorderWidth: onClient ? (Hypr.options["general:border_size"] ?? 1) : 2
property real realRounding: onClient ? (Hypr.options.decoration.rounding ?? 0) : 0 property real realRounding: onClient ? (Hypr.options["decoration:rounding"] ?? 0) : 0
property real rsx: Math.min(sx, ex) property real rsx: Math.min(sx, ex)
property real rsy: Math.min(sy, ey) property real rsy: Math.min(sy, ey)
readonly property real scaleRatio: Hypr.monitorFor(screen).scale
required property ShellScreen screen required property ShellScreen screen
property real sh: Math.abs(sy - ey) property real sh: Math.abs(sy - ey)
readonly property color shadowColor: Hypr.options.decoration.shadow.color
readonly property var shadowOffset: Hypr.options.decoration.shadow.offset
readonly property int shadowRange: Hypr.options.decoration.shadow.range
readonly property int shadowRenderPower: Hypr.options.decoration.shadow.render_power
property real ssx property real ssx
property real ssy property real ssy
property real sw: Math.abs(sx - ex) property real sw: Math.abs(sx - ex)
@@ -71,7 +66,7 @@ MouseArea {
function save(): void { function save(): void {
const tmpfile = Qt.resolvedUrl(`/tmp/zshell-picker-${Quickshell.processId}-${Date.now()}.png`); const tmpfile = Qt.resolvedUrl(`/tmp/zshell-picker-${Quickshell.processId}-${Date.now()}.png`);
const cmd = Config.screenshot.enable_pp ? ["zshell-img-tools", "--scale", root.scaleRatio, "--shadow-blur-radius", root.shadowRange, "--image"] : ["swappy", "-f"]; const cmd = Config.screenshot.enable_pp ? ["zshell-img-tools", "--image"] : ["swappy", "-f"];
ZShellIo.saveItem(screencopy, tmpfile, Qt.rect(Math.ceil(rsx), Math.ceil(rsy), Math.floor(sw), Math.floor(sh)), path => Quickshell.execDetached([...cmd, path])); ZShellIo.saveItem(screencopy, tmpfile, Qt.rect(Math.ceil(rsx), Math.ceil(rsy), Math.floor(sw), Math.floor(sh)), path => Quickshell.execDetached([...cmd, path]));
closeAnim.start(); closeAnim.start();
} }
-1
View File
@@ -17,7 +17,6 @@ Singleton {
property var disks: [] property var disks: []
property real gpuMemTotal: 0 property real gpuMemTotal: 0
property real gpuMemUsed property real gpuMemUsed
property string gpuName
property real gpuPerc property real gpuPerc
property real gpuTemp property real gpuTemp
readonly property string gpuType: Config.services.gpuType.toUpperCase() || autoGpuType readonly property string gpuType: Config.services.gpuType.toUpperCase() || autoGpuType
+3 -1
View File
@@ -53,12 +53,14 @@ Searcher {
}; };
root.crops = updated; root.crops = updated;
monitorCrops.writeAdapter();
monitorCrops.reload();
} }
function setWallpaper(path: string): void { function setWallpaper(path: string): void {
actualCurrent = path; actualCurrent = path;
WallpaperPath.currentWallpaperPath = path; WallpaperPath.currentWallpaperPath = path;
Quickshell.screens.forEach(n => setCrop(n.name, Qt.rect(0, 0, 1, 1), Qt.rect(0, 0, 0, 0), 1.0)); Quickshell.screens.forEach(n => setCrop(n.name, Qt.rect(0, 0, 0, 0), Qt.rect(0, 0, 0, 0), 1.0));
Quickshell.execDetached(["zshell-cli", "wallpaper", "lockscreen", "--input-image", `${root.actualCurrent}`, "--output-path", `${Paths.state}/lockscreen_bg.png`, "--blur-amount", `${Config.lock.blurAmount}`]); Quickshell.execDetached(["zshell-cli", "wallpaper", "lockscreen", "--input-image", `${root.actualCurrent}`, "--output-path", `${Paths.state}/lockscreen_bg.png`, "--blur-amount", `${Config.lock.blurAmount}`]);
if (Config.general.color.schemeGeneration) if (Config.general.color.schemeGeneration)
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--image-path", `${root.actualCurrent}`, "--scheme", `${Config.colors.schemeType}`, "--mode", `${Config.general.color.mode}`]); Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--image-path", `${root.actualCurrent}`, "--scheme", `${Config.colors.schemeType}`, "--mode", `${Config.general.color.mode}`]);
+68
View File
@@ -0,0 +1,68 @@
import QtQuick
import QtQuick.Shapes
import qs.Components
import qs.Config
ShapePath {
id: root
readonly property bool flatten: wrapper.height < rounding * 2
property real ibr: invertBottomRounding ? -1 : 1
required property bool invertBottomRounding
property real rounding: Appearance.rounding.smallest
readonly property real roundingY: flatten ? wrapper.height / 2 : rounding
required property Wrapper wrapper
fillColor: DynamicColors.palette.m3surface
strokeWidth: -1
Behavior on fillColor {
CAnim {
}
}
PathArc {
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: root.roundingY
}
PathLine {
relativeX: 0
relativeY: root.wrapper.height - root.roundingY - root.roundingY * root.ibr
}
PathArc {
direction: root.invertBottomRounding ? PathArc.Clockwise : PathArc.Counterclockwise
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: root.roundingY * root.ibr
}
PathLine {
relativeX: root.wrapper.width - root.rounding * 2
relativeY: 0
}
PathArc {
direction: PathArc.Counterclockwise
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: -root.roundingY
}
PathLine {
relativeX: 0
relativeY: -(root.wrapper.height - root.roundingY * 2)
}
PathArc {
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: -root.roundingY
}
}
+65
View File
@@ -0,0 +1,65 @@
import qs.Components
import qs.Config
import QtQuick
import QtQuick.Shapes
ShapePath {
id: root
readonly property bool flatten: wrapper.height < rounding * 2
readonly property real rounding: Appearance.rounding.normal
readonly property real roundingY: flatten ? wrapper.height / 2 : rounding
required property Wrapper wrapper
fillColor: DynamicColors.palette.m3surface
strokeWidth: -1
Behavior on fillColor {
CAnim {
}
}
PathArc {
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: root.roundingY
}
PathLine {
relativeX: 0
relativeY: root.wrapper.height - root.roundingY * 2
}
PathArc {
direction: PathArc.Counterclockwise
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: root.roundingY
}
PathLine {
relativeX: root.wrapper.width - root.rounding * 2
relativeY: 0
}
PathArc {
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: root.roundingY
}
PathLine {
relativeX: 0
relativeY: -(root.wrapper.height)
}
PathArc {
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: -root.roundingY
}
}
+1 -1
View File
@@ -20,7 +20,7 @@ Item {
required property PersistentProperties visibilities required property PersistentProperties visibilities
implicitHeight: content.implicitHeight implicitHeight: content.implicitHeight
implicitWidth: content.implicitWidth || 854 implicitWidth: content.implicitWidth || 854 // Hard coded fallback for first open
opacity: 1 - offsetScale opacity: 1 - offsetScale
visible: offsetScale < 1 visible: offsetScale < 1
+66
View File
@@ -0,0 +1,66 @@
import QtQuick
import QtQuick.Shapes
import qs.Components
import qs.Config
ShapePath {
id: root
readonly property bool flatten: wrapper.height < rounding * 2
readonly property real rounding: Appearance.rounding.normal
readonly property real roundingY: flatten ? wrapper.height / 2 : rounding
required property Wrapper wrapper
fillColor: DynamicColors.palette.m3surface
strokeWidth: -1
Behavior on fillColor {
CAnim {
}
}
PathArc {
direction: PathArc.Counterclockwise
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: -root.roundingY
}
PathLine {
relativeX: 0
relativeY: -(root.wrapper.height - root.roundingY * 2)
}
PathArc {
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: -root.roundingY
}
PathLine {
relativeX: root.wrapper.width - root.rounding * 2
relativeY: 0
}
PathArc {
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: root.roundingY
}
PathLine {
relativeX: 0
relativeY: root.wrapper.height - root.roundingY * 2
}
PathArc {
direction: PathArc.Counterclockwise
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: root.roundingY
}
}
+6 -5
View File
@@ -9,17 +9,18 @@ Item {
id: root id: root
property int contentHeight property int contentHeight
property real offsetScale: shouldBeActive ? 0 : 1
required property var panels required property var panels
required property ShellScreen screen required property ShellScreen screen
readonly property bool shouldBeActive: visibilities.dock && Config.dock.enable
required property PersistentProperties visibilities required property PersistentProperties visibilities
readonly property bool shouldBeActive: visibilities.dock
property real offsetScale: shouldBeActive ? 0 : 1
visible: offsetScale < 1
anchors.bottomMargin: (-implicitHeight - 5) * offsetScale anchors.bottomMargin: (-implicitHeight - 5) * offsetScale
implicitHeight: content.implicitHeight implicitHeight: content.implicitHeight
implicitWidth: content.implicitWidth || 400 implicitWidth: content.implicitWidth || 400
opacity: 1 - offsetScale opacity: 1 - offsetScale
visible: offsetScale < 1
Behavior on offsetScale { Behavior on offsetScale {
Anim { Anim {
@@ -31,10 +32,10 @@ Item {
Loader { Loader {
id: content id: content
active: root.shouldBeActive || root.visible
anchors.left: parent.left anchors.left: parent.left
anchors.top: parent.top anchors.top: parent.top
asynchronous: true
active: root.shouldBeActive || root.visible
sourceComponent: Content { sourceComponent: Content {
panels: root.panels panels: root.panels
+66
View File
@@ -0,0 +1,66 @@
import QtQuick
import QtQuick.Shapes
import qs.Components
import qs.Config
ShapePath {
id: root
readonly property bool flatten: wrapper.width < rounding * 2
readonly property real rounding: Appearance.rounding.normal
readonly property real roundingX: flatten ? wrapper.width / 2 : rounding
required property Wrapper wrapper
fillColor: DynamicColors.palette.m3surface
strokeWidth: -1
Behavior on fillColor {
CAnim {
}
}
PathArc {
direction: PathArc.Counterclockwise
radiusX: Math.min(root.rounding, root.wrapper.width)
radiusY: root.rounding
relativeX: root.roundingX
relativeY: root.rounding
}
PathLine {
relativeX: root.wrapper.width - root.roundingX * 2
relativeY: 0
}
PathArc {
radiusX: Math.min(root.rounding, root.wrapper.width)
radiusY: root.rounding
relativeX: root.roundingX
relativeY: root.rounding
}
PathLine {
relativeX: 0
relativeY: root.wrapper.height - root.rounding * 2
}
PathArc {
radiusX: Math.min(root.rounding, root.wrapper.width)
radiusY: root.rounding
relativeX: -root.roundingX
relativeY: root.rounding
}
PathLine {
relativeX: -(root.wrapper.width - root.roundingX * 2)
relativeY: 0
}
PathArc {
direction: PathArc.Counterclockwise
radiusX: Math.min(root.rounding, root.wrapper.width)
radiusY: root.rounding
relativeX: -root.roundingX
relativeY: root.rounding
}
}
+4 -4
View File
@@ -44,10 +44,10 @@ Item {
Loader { Loader {
id: icon id: icon
active: root.shouldBeActive || root.visible active: Qt.binding(() => root.shouldBeActive || root.visible)
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
asynchronous: true height: content.contentItem.height
opacity: root.expanded ? 0 : 1 opacity: root.expanded ? 0 : 1
Behavior on opacity { Behavior on opacity {
@@ -63,10 +63,8 @@ Item {
Loader { Loader {
id: content id: content
active: root.shouldBeActive || root.visible
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
asynchronous: true
opacity: root.expanded ? 1 : 0 opacity: root.expanded ? 1 : 0
Behavior on opacity { Behavior on opacity {
@@ -77,5 +75,7 @@ Item {
drawing: root.drawing drawing: root.drawing
visibilities: root.visibilities visibilities: root.visibilities
} }
Component.onCompleted: active = Qt.binding(() => root.shouldBeActive || root.visible)
} }
} }
+66
View File
@@ -0,0 +1,66 @@
import QtQuick
import QtQuick.Shapes
import qs.Components
import qs.Config
ShapePath {
id: root
readonly property bool flatten: wrapper.height < rounding * 2
readonly property real rounding: Appearance.rounding.smallest + 5
readonly property real roundingY: flatten ? wrapper.height / 2 : rounding
required property Wrapper wrapper
fillColor: DynamicColors.palette.m3surface
strokeWidth: -1
Behavior on fillColor {
CAnim {
}
}
PathArc {
direction: PathArc.Counterclockwise
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: -root.roundingY
}
PathLine {
relativeX: 0
relativeY: -(root.wrapper.height - root.roundingY * 2)
}
PathArc {
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: -root.roundingY
}
PathLine {
relativeX: root.wrapper.width - root.rounding * 2
relativeY: 0
}
PathArc {
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: root.roundingY
}
PathLine {
relativeX: 0
relativeY: root.wrapper.height - root.roundingY * 2
}
PathArc {
direction: PathArc.Counterclockwise
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: root.roundingY
}
}
+60 -16
View File
@@ -4,7 +4,6 @@ import Quickshell
import QtQuick import QtQuick
import qs.Components import qs.Components
import qs.Config import qs.Config
import qs.Modules.Launcher.Services
Item { Item {
id: root id: root
@@ -12,24 +11,34 @@ Item {
property int contentHeight property int contentHeight
readonly property real maxHeight: { readonly property real maxHeight: {
let max = screen.height - Appearance.spacing.large * 2; let max = screen.height - Appearance.spacing.large * 2;
if (visibilities.resources && panels.resourcesWrapper.x + panels.resourcesWrapper.width > root.x) if (visibilities.resources)
max -= panels.resources.nonAnimHeight; max -= panels.resources.nonAnimHeight;
if (panels.popouts.hasCurrent) if (visibilities.dashboard && panels.dashboard.x < root.x + root.implicitWidth)
if (panels.popouts.current.x + panels.popouts.current.width > root.x && panels.popouts.current.x < root.x + root.width) max -= panels.dashboard.nonAnimHeight;
max -= panels.popouts.nonAnimHeight; if (panels.popouts.currentName.startsWith("updates"))
max -= panels.popouts.nonAnimHeight;
return max; return max;
} }
property real offsetScale: shouldBeActive ? 0 : 1
required property var panels required property var panels
required property ShellScreen screen required property ShellScreen screen
readonly property bool shouldBeActive: visibilities.launcher
required property PersistentProperties visibilities required property PersistentProperties visibilities
readonly property bool shouldBeActive: visibilities.launcher
property real offsetScale: shouldBeActive ? 0 : 1
onShouldBeActiveChanged: {
if (shouldBeActive) {
implicitHeight = Qt.binding(() => content.implicitHeight);
timer.stop();
} else {
implicitHeight = implicitHeight;
}
}
visible: offsetScale < 1
anchors.bottomMargin: (-implicitHeight - 5) * offsetScale anchors.bottomMargin: (-implicitHeight - 5) * offsetScale
implicitHeight: content.implicitHeight implicitHeight: content.implicitHeight
implicitWidth: content.implicitWidth || 400 implicitWidth: content.implicitWidth || 400
opacity: 1 - offsetScale opacity: 1 - offsetScale
visible: offsetScale < 1
Behavior on offsetScale { Behavior on offsetScale {
Anim { Anim {
@@ -38,26 +47,61 @@ Item {
} }
} }
Component.onCompleted: Qt.callLater(() => Apps) onMaxHeightChanged: timer.start()
onShouldBeActiveChanged: {
if (shouldBeActive) Connections {
implicitHeight = Qt.binding(() => content.implicitHeight); function onEnabledChanged(): void {
else timer.start();
implicitHeight = implicitHeight; }
function onMaxShownChanged(): void {
timer.start();
}
target: Config.launcher
}
Connections {
function onValuesChanged(): void {
if (DesktopEntries.applications.values.length < Config.launcher.maxAppsShown)
timer.start();
}
target: DesktopEntries.applications
}
Timer {
id: timer
interval: Appearance.anim.durations.small
onRunningChanged: {
if (running && !root.shouldBeActive) {
content.visible = false;
content.active = true;
} else {
root.contentHeight = Math.min(root.maxHeight, content.implicitHeight);
content.active = Qt.binding(() => root.shouldBeActive || root.visible);
content.visible = true;
}
}
} }
Loader { Loader {
id: content id: content
active: root.shouldBeActive || root.visible active: false
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top anchors.top: parent.top
asynchronous: true
sourceComponent: Content { sourceComponent: Content {
maxHeight: root.maxHeight maxHeight: root.maxHeight
panels: root.panels panels: root.panels
visibilities: root.visibilities visibilities: root.visibilities
Component.onCompleted: root.contentHeight = implicitHeight
} }
Component.onCompleted: timer.start()
} }
} }
+59
View File
@@ -0,0 +1,59 @@
import qs.Components
import qs.Config
import QtQuick
import QtQuick.Shapes
ShapePath {
id: root
readonly property bool flatten: wrapper.height < rounding * 2
readonly property real rounding: 8
readonly property real roundingY: flatten ? wrapper.height / 2 : rounding
required property var sidebar
required property Wrapper wrapper
fillColor: DynamicColors.palette.m3surface
strokeWidth: -1
Behavior on fillColor {
CAnim {
}
}
PathLine {
relativeX: -(root.wrapper.width + root.rounding)
relativeY: 0
}
PathArc {
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: root.roundingY
}
PathLine {
relativeX: 0
relativeY: root.wrapper.height - root.roundingY * 2
}
PathArc {
direction: PathArc.Counterclockwise
radiusX: root.sidebar.notifsRoundingX
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.sidebar.notifsRoundingX
relativeY: root.roundingY
}
PathLine {
relativeX: root.wrapper.height > 0 ? root.wrapper.width - root.rounding - root.sidebar.notifsRoundingX : root.wrapper.width
relativeY: 0
}
PathArc {
radiusX: root.rounding
radiusY: root.rounding
relativeX: root.rounding
relativeY: root.rounding
}
}
+45 -3
View File
@@ -8,7 +8,7 @@ import QtQuick
Item { Item {
id: root id: root
readonly property int padding: Appearance.padding.smaller readonly property int padding: 6
required property Item panels required property Item panels
required property PersistentProperties visibilities required property PersistentProperties visibilities
@@ -54,7 +54,7 @@ Item {
anchors.fill: parent anchors.fill: parent
anchors.margins: root.padding anchors.margins: root.padding
color: "transparent" color: "transparent"
radius: Appearance.rounding.normal - root.padding radius: Appearance.rounding.smallest / 2
CustomListView { CustomListView {
id: list id: list
@@ -72,7 +72,7 @@ Item {
required property NotifServer.Notif modelData required property NotifServer.Notif modelData
readonly property alias nonAnimHeight: notif.nonAnimHeight readonly property alias nonAnimHeight: notif.nonAnimHeight
implicitHeight: notif.implicitHeight + (idx === 0 ? 0 : Appearance.spacing.small) implicitHeight: notif.implicitHeight + (idx === 0 ? 0 : 8)
implicitWidth: notif.implicitWidth implicitWidth: notif.implicitWidth
ListView.onRemove: removeAnim.start() ListView.onRemove: removeAnim.start()
@@ -151,6 +151,48 @@ Item {
property: "y" property: "y"
} }
} }
ExtraIndicator {
anchors.top: parent.top
extra: {
const count = list.count;
if (count === 0)
return 0;
const scrollY = list.contentY;
let height = 0;
for (let i = 0; i < count; i++) {
height += (list.itemAtIndex(i)?.nonAnimHeight ?? 0) + 8;
if (height - 8 >= scrollY)
return i;
}
return count;
}
}
ExtraIndicator {
anchors.bottom: parent.bottom
extra: {
const count = list.count;
if (count === 0)
return 0;
const scrollY = list.contentHeight - (list.contentY + list.height);
let height = 0;
for (let i = count - 1; i >= 0; i--) {
height += (list.itemAtIndex(i)?.nonAnimHeight ?? 0) + 8;
if (height - 8 >= scrollY)
return count - i - 1;
}
return 0;
}
}
} }
} }
@@ -0,0 +1,54 @@
import QtQuick
import QtQuick.Shapes
import qs.Components
import qs.Config
ShapePath {
id: root
readonly property bool flatten: wrapper.width < rounding * 2
readonly property real notifsRoundingX: panels.notifications.height > 0 && notifsWidthDiff < rounding * 2 ? notifsWidthDiff / 2 : rounding
readonly property real notifsWidthDiff: panels.notifications.width - wrapper.width
required property var panels
readonly property real rounding: Config.barConfig.rounding
readonly property real utilsRoundingX: utilsWidthDiff < rounding * 2 ? utilsWidthDiff / 2 : rounding
readonly property real utilsWidthDiff: panels.utilities.width - wrapper.width
required property Wrapper wrapper
fillColor: DynamicColors.palette.m3surface
strokeWidth: -1
Behavior on fillColor {
CAnim {
}
}
PathLine {
relativeX: -root.wrapper.width - root.notifsRoundingX
relativeY: 0
}
PathArc {
radiusX: root.notifsRoundingX
radiusY: root.rounding
relativeX: root.notifsRoundingX
relativeY: root.rounding
}
PathLine {
relativeX: 0
relativeY: root.wrapper.height - root.rounding * 2
}
PathArc {
radiusX: root.utilsRoundingX
radiusY: root.rounding
relativeX: -root.utilsRoundingX
relativeY: root.rounding
}
PathLine {
relativeX: root.wrapper.width + root.utilsRoundingX
relativeY: 0
}
}
+66
View File
@@ -0,0 +1,66 @@
import QtQuick
import QtQuick.Shapes
import qs.Components
import qs.Config
ShapePath {
id: root
readonly property bool flatten: wrapper.width < rounding * 2
readonly property real rounding: 10
readonly property real roundingX: flatten ? wrapper.width / 2 : rounding
required property Wrapper wrapper
fillColor: DynamicColors.palette.m3surface
strokeWidth: -1
Behavior on fillColor {
CAnim {
}
}
PathArc {
radiusX: Math.min(root.rounding, root.wrapper.width)
radiusY: root.rounding
relativeX: -root.roundingX
relativeY: root.rounding
}
PathLine {
relativeX: -(root.wrapper.width - root.roundingX * 3)
relativeY: 0
}
PathArc {
direction: PathArc.Counterclockwise
radiusX: Math.min(root.rounding * 2, root.wrapper.width)
radiusY: root.rounding * 2
relativeX: -root.roundingX * 2
relativeY: root.rounding * 2
}
PathLine {
relativeX: 0
relativeY: root.wrapper.height - root.rounding * 4
}
PathArc {
direction: PathArc.Counterclockwise
radiusX: Math.min(root.rounding * 2, root.wrapper.width)
radiusY: root.rounding * 2
relativeX: root.roundingX * 2
relativeY: root.rounding * 2
}
PathLine {
relativeX: root.wrapper.width - root.roundingX * 3
relativeY: 0
}
PathArc {
radiusX: Math.min(root.rounding, root.wrapper.width)
radiusY: root.rounding
relativeX: root.roundingX
relativeY: root.rounding
}
}
+65
View File
@@ -0,0 +1,65 @@
import qs.Components
import qs.Config
import QtQuick
import QtQuick.Shapes
ShapePath {
id: root
readonly property bool flatten: wrapper.height < rounding * 2
readonly property real rounding: Appearance.rounding.normal
readonly property real roundingY: flatten ? wrapper.height / 2 : rounding
required property Wrapper wrapper
fillColor: DynamicColors.palette.m3surface
strokeWidth: -1
Behavior on fillColor {
CAnim {
}
}
PathArc {
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: root.roundingY
}
PathLine {
relativeX: 0
relativeY: root.wrapper.height
}
PathArc {
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: -root.roundingY
}
PathLine {
relativeX: root.wrapper.width - root.rounding * 2
relativeY: 0
}
PathArc {
direction: PathArc.Counterclockwise
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: -root.roundingY
}
PathLine {
relativeX: 0
relativeY: -(root.wrapper.height - root.roundingY * 2)
}
PathArc {
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: -root.roundingY
}
}
+3 -2
View File
@@ -8,7 +8,7 @@ import qs.Config
Item { Item {
id: root id: root
readonly property real nonAnimHeight: content.item?.nonAnimHeight ?? 0 readonly property real nonAnimHeight: state === "visible" ? (content.item?.nonAnimHeight ?? 0) : 0
property real offsetScale: shouldBeActive ? 0 : 1 property real offsetScale: shouldBeActive ? 0 : 1
readonly property bool shouldBeActive: root.visibilities.resources readonly property bool shouldBeActive: root.visibilities.resources
required property PersistentProperties visibilities required property PersistentProperties visibilities
@@ -31,7 +31,8 @@ Item {
id: content id: content
active: root.shouldBeActive || root.visible active: root.shouldBeActive || root.visible
anchors.centerIn: parent anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
sourceComponent: Content { sourceComponent: Content {
padding: Appearance.padding.normal padding: Appearance.padding.normal
+66
View File
@@ -0,0 +1,66 @@
import QtQuick
import QtQuick.Shapes
import qs.Components
import qs.Config
ShapePath {
id: root
readonly property bool flatten: wrapper.height < rounding * 2
readonly property real rounding: Appearance.rounding.large
readonly property real roundingY: flatten ? wrapper.height / 2 : rounding
required property Wrapper wrapper
fillColor: DynamicColors.palette.m3surface
strokeWidth: -1
Behavior on fillColor {
CAnim {
}
}
PathArc {
radiusX: root.rounding
radiusY: Math.min(root.roundingY, root.wrapper.height)
relativeX: root.rounding
relativeY: root.roundingY
}
PathLine {
relativeX: 0
relativeY: root.wrapper.height - root.roundingY * 2
}
PathArc {
direction: PathArc.Counterclockwise
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: root.roundingY
}
PathLine {
relativeX: root.wrapper.width - root.rounding * 2
relativeY: 0
}
PathArc {
direction: PathArc.Counterclockwise
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: -root.roundingY
}
PathLine {
relativeX: 0
relativeY: -(root.wrapper.height - root.roundingY * 2)
}
PathArc {
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: -root.roundingY
}
}
+6
View File
@@ -116,6 +116,12 @@ Item {
key: "updates" key: "updates"
name: "Updates" name: "Updates"
} }
ListElement {
icon: "extension"
key: "plugins"
name: "Extensions"
}
} }
CustomClippingRect { CustomClippingRect {
-15
View File
@@ -56,21 +56,6 @@ SettingsPage {
} }
} }
SettingsSection {
sectionId: "Tray"
SettingsHeader {
name: "System tray"
}
SettingSpinBox {
min: 16
name: "Tray icon size"
object: Config.barConfig.tray
setting: "trayIconSize"
}
}
SettingsSection { SettingsSection {
sectionId: "Popouts" sectionId: "Popouts"
+18
View File
@@ -0,0 +1,18 @@
import qs.Modules.Settings.Controls
import qs.Config
SettingsPage {
SettingsSection {
sectionId: "Plugins"
SettingsHeader {
name: "Plugins"
}
SettingBarEntryList {
name: "Enable or disable plugins"
object: Config.plugins
setting: "entries"
}
}
}
+14 -27
View File
@@ -43,7 +43,7 @@ SettingsPage {
} }
Separator { Separator {
shouldBeActive: Config.screenshot.mode === "manual" visible: Config.screenshot.mode === "manual"
} }
SettingSpinBox { SettingSpinBox {
@@ -51,34 +51,34 @@ SettingsPage {
name: "Corner radius" name: "Corner radius"
object: Config.screenshot object: Config.screenshot
setting: "corner_radius" setting: "corner_radius"
shouldBeActive: Config.screenshot.mode === "manual"
step: 1 step: 1
visible: Config.screenshot.mode === "manual"
} }
Separator { Separator {
shouldBeActive: Config.screenshot.mode === "manual" visible: Config.screenshot.mode === "manual"
} }
SettingSwitch { SettingSwitch {
name: "Enable drop shadow" name: "Enable drop shadow"
object: Config.screenshot object: Config.screenshot
setting: "drop_shadow" setting: "drop_shadow"
shouldBeActive: Config.screenshot.mode === "manual" visible: Config.screenshot.mode === "manual"
} }
Separator { Separator {
shouldBeActive: Config.screenshot.mode === "manual" visible: Config.screenshot.mode === "manual"
} }
SettingSwitch { SettingSwitch {
name: "Enable rounded corners" name: "Enable rounded corners"
object: Config.screenshot object: Config.screenshot
setting: "rounded_corners" setting: "rounded_corners"
shouldBeActive: Config.screenshot.mode === "manual" visible: Config.screenshot.mode === "manual"
} }
Separator { Separator {
shouldBeActive: Config.screenshot.mode === "manual" visible: Config.screenshot.mode === "manual"
} }
SettingSpinBox { SettingSpinBox {
@@ -86,36 +86,23 @@ SettingsPage {
name: "Shadow blur radius" name: "Shadow blur radius"
object: Config.screenshot object: Config.screenshot
setting: "shadow_blur_radius" setting: "shadow_blur_radius"
shouldBeActive: Config.screenshot.mode === "manual"
step: 1 step: 1
visible: Config.screenshot.mode === "manual"
} }
Separator { Separator {
shouldBeActive: Config.screenshot.mode === "manual" visible: Config.screenshot.mode === "manual"
} }
SettingSwitch { SettingSwitch {
name: "Shadow color broken atm" name: "Shadow color broken atm"
object: Config.Screenshot object: Config.Screenshot
setting: "shadow_color" setting: "shadow_color"
shouldBeActive: Config.screenshot.mode === "manual" visible: Config.screenshot.mode === "manual"
} }
Separator { Separator {
shouldBeActive: Config.screenshot.mode === "manual" visible: Config.screenshot.mode === "manual"
}
SettingSpinBox {
min: 1
name: "Shadow passes"
object: Config.screenshot
setting: "shadow_blur_passes"
shouldBeActive: Config.screenshot.mode === "manual"
step: 1
}
Separator {
shouldBeActive: Config.screenshot.mode === "manual"
} }
SettingSpinBox { SettingSpinBox {
@@ -123,12 +110,12 @@ SettingsPage {
name: "Shadow offset X" name: "Shadow offset X"
object: Config.screenshot object: Config.screenshot
setting: "shadow_offset_x" setting: "shadow_offset_x"
shouldBeActive: Config.screenshot.mode === "manual"
step: 1 step: 1
visible: Config.screenshot.mode === "manual"
} }
Separator { Separator {
shouldBeActive: Config.screenshot.mode === "manual" visible: Config.screenshot.mode === "manual"
} }
SettingSpinBox { SettingSpinBox {
@@ -136,8 +123,8 @@ SettingsPage {
name: "Shadow offset Y" name: "Shadow offset Y"
object: Config.screenshot object: Config.screenshot
setting: "shadow_offset_y" setting: "shadow_offset_y"
shouldBeActive: Config.screenshot.mode === "manual"
step: 1 step: 1
visible: Config.screenshot.mode === "manual"
} }
} }
} }
+10 -1
View File
@@ -79,6 +79,8 @@ Item {
stack.push(screenshot); stack.push(screenshot);
else if (currentCategory === "updates") else if (currentCategory === "updates")
stack.push(updates); stack.push(updates);
else if (currentCategory === "plugins")
stack.push(plugins);
} }
target: root target: root
@@ -134,7 +136,7 @@ Item {
anchors.right: parent.right anchors.right: parent.right
anchors.top: searchBar.bottom anchors.top: searchBar.bottom
anchors.topMargin: Appearance.spacing.smaller anchors.topMargin: Appearance.spacing.smaller
color: DynamicColors.tPalette.m3surfaceContainer color: DynamicColors.tPalette.m3surfaceContainerLowest
radius: Appearance.rounding.normal radius: Appearance.rounding.normal
StackView { StackView {
@@ -245,4 +247,11 @@ Item {
Cat.SystemUpdates { Cat.SystemUpdates {
} }
} }
Component {
id: plugins
Cat.Plugins {
}
}
} }
+60 -92
View File
@@ -80,26 +80,12 @@ Item {
required property ShellScreen modelData required property ShellScreen modelData
function applyCrop(): void { function applyCrop(): void {
if (!cropRectLoader.item) return; const croprect = cropRect.mapToItem(scaledImg, 0, 0, cropRect.width, cropRect.height);
const cropRect = cropRectLoader.item; const upscaledRect = Qt.rect((croprect.x - cropRect.imageX) / scaledImg.paintedWidth, (croprect.y - cropRect.imageY) / scaledImg.paintedHeight, croprect.width / scaledImg.paintedWidth, croprect.height / scaledImg.paintedHeight);
Wallpapers.setCrop(delegate.modelData.name, upscaledRect, croprect, cropRect.zoom);
// We need to calculate the exact percentage coordinates that map perfectly
// to our C++ backend, regardless of current display scaling
const cropXPercent = (cropRect.x - cropRect.imageX) / scaledImg.paintedWidth;
const cropYPercent = (cropRect.y - cropRect.imageY) / scaledImg.paintedHeight;
const cropWidthPercent = cropRect.width / scaledImg.paintedWidth;
const cropHeightPercent = cropRect.height / scaledImg.paintedHeight;
const finalRect = Qt.rect(cropXPercent, cropYPercent, cropWidthPercent, cropHeightPercent);
// We just pass the percentages directly to the backend
Wallpapers.setCrop(delegate.modelData.name, finalRect, finalRect, cropRect.zoom);
} }
function zoomClipRect(zoom: real): void { function zoomClipRect(zoom: real): void {
if (!cropRectLoader.item) return;
const cropRect = cropRectLoader.item;
let oldCenterX = cropRect.x + cropRect.width * 0.5; let oldCenterX = cropRect.x + cropRect.width * 0.5;
let oldCenterY = cropRect.y + cropRect.height * 0.5; let oldCenterY = cropRect.y + cropRect.height * 0.5;
@@ -142,7 +128,7 @@ Item {
Layout.preferredHeight: 10 Layout.preferredHeight: 10
from: 1.0 from: 1.0
to: 5.0 to: 5.0
value: cropRectLoader.item ? cropRectLoader.item.zoom : 1.0 value: cropRect.zoom
onMoved: { onMoved: {
delegate.zoomClipRect(value); delegate.zoomClipRect(value);
@@ -170,20 +156,15 @@ Item {
sourceSize.width: parent.width sourceSize.width: parent.width
onPaintedWidthChanged: { onPaintedWidthChanged: {
if (paintedWidth > 0 && cropRectLoader.item) { if (paintedWidth > 0) {
cropRectLoader.item.restoreFromData(); scaledImg.displayData = Wallpapers.getCrop(delegate.modelData.name);
} cropRect.zoom = Wallpapers.getCrop(delegate.modelData.name).zoom;
} cropRect.restoreFromData();
onSourceChanged: {
if (cropRectLoader.item) {
cropRectLoader.item.restoreFromData();
}
}
onStatusChanged: {
if (scaledImg.status == Image.Ready && cropRectLoader.item) {
cropRectLoader.item.restoreFromData();
} }
} }
onSourceChanged: cropRect.clampToBounds()
onStatusChanged: if (scaledImg.status == Image.Ready)
cropRect.clampToBounds()
CustomText { CustomText {
id: monitorId id: monitorId
@@ -196,85 +177,72 @@ Item {
text: delegate.modelData.name text: delegate.modelData.name
} }
Loader { CustomRect {
id: cropRectLoader id: cropRect
active: scaledImg.paintedWidth > 0 && scaledImg.status == Image.Ready
sourceComponent: Component {
CustomRect {
id: cropRect
property real aspectRatio: delegate.modelData.width / delegate.modelData.height property real aspectRatio: delegate.modelData.width / delegate.modelData.height
readonly property real baseHeight: baseWidth / aspectRatio readonly property real baseHeight: baseWidth / aspectRatio
readonly property real baseWidth: { readonly property real baseWidth: {
let fittedHeight = scaledImg.paintedHeight; let fittedHeight = scaledImg.paintedHeight;
let fittedWidth = fittedHeight * aspectRatio; let fittedWidth = fittedHeight * aspectRatio;
if (fittedWidth > scaledImg.paintedWidth) { if (fittedWidth > scaledImg.paintedWidth) {
fittedWidth = scaledImg.paintedWidth; fittedWidth = scaledImg.paintedWidth;
fittedHeight = fittedWidth / aspectRatio; fittedHeight = fittedWidth / aspectRatio;
} }
return fittedWidth; return fittedWidth;
} }
readonly property real imageX: (scaledImg.width - scaledImg.paintedWidth) / 2 readonly property real imageX: (scaledImg.width - scaledImg.paintedWidth) / 2
readonly property real imageY: (scaledImg.height - scaledImg.paintedHeight) / 2 readonly property real imageY: (scaledImg.height - scaledImg.paintedHeight) / 2
property real imgAspectRatio: scaledImg.paintedWidth / scaledImg.paintedHeight property real imgAspectRatio: scaledImg.paintedWidth / scaledImg.paintedHeight
property real zoom: 1.0 property real zoom: scaledImg.displayData.zoom
function centerInImage() { function centerInImage() {
x = imageX + (scaledImg.paintedWidth - width) / 2; x = imageX + (scaledImg.paintedWidth - width) / 2;
y = imageY + (scaledImg.paintedHeight - height) / 2; y = imageY + (scaledImg.paintedHeight - height) / 2;
} }
function clampToBounds() { function clampToBounds() {
x = Math.max(imageX, Math.min(x, imageX + scaledImg.paintedWidth - width)); x = Math.max(imageX, Math.min(x, imageX + scaledImg.paintedWidth - width));
y = Math.max(imageY, Math.min(y, imageY + scaledImg.paintedHeight - height)); y = Math.max(imageY, Math.min(y, imageY + scaledImg.paintedHeight - height));
} }
function restoreFromData() { function restoreFromData() {
let data = Wallpapers.getCrop(delegate.modelData.name); let data = scaledImg.displayData;
if (data && (Math.abs(data.x) > 0.001 || Math.abs(data.y) > 0.001 || Math.abs(data.width - 1.0) > 0.001 || Math.abs(data.height - 1.0) > 0.001)) { if (data && data.scaledX !== 0 || data.scaledY !== 0 || data.scaledWidth !== 0 || data.scaledHeight !== 0) {
zoom = data.zoom > 0 ? data.zoom : 1.0; x = data.scaledX;
x = imageX + (data.x * scaledImg.paintedWidth); y = data.scaledY;
y = imageY + (data.y * scaledImg.paintedHeight);
clampToBounds();
} else {
zoom = 1.0;
centerInImage();
}
}
border.color: DynamicColors.palette.m3primary clampToBounds();
border.width: 2 } else {
height: baseHeight / zoom zoom = 1.0;
opacity: 1 centerInImage();
width: baseWidth / zoom
Behavior on opacity {
Anim {
}
}
Component.onCompleted: {
restoreFromData();
}
onHeightChanged: clampToBounds()
onWidthChanged: clampToBounds()
} }
} }
border.color: DynamicColors.palette.m3primary
border.width: 2
height: baseHeight / zoom
opacity: 1
width: baseWidth / zoom
Behavior on opacity {
Anim {
}
}
Component.onCompleted: clampToBounds()
onHeightChanged: clampToBounds()
onWidthChanged: clampToBounds()
} }
MouseArea { MouseArea {
id: mouse id: mouse
function updateCrop(mouseX, mouseY) { function updateCrop(mouseX, mouseY) {
if (!cropRectLoader.item) return;
const cropRect = cropRectLoader.item;
let nx = mouseX - cropRect.width * 0.5; let nx = mouseX - cropRect.width * 0.5;
let ny = mouseY - cropRect.height * 0.5; let ny = mouseY - cropRect.height * 0.5;
+2 -1
View File
@@ -32,9 +32,10 @@ Item {
Loader { Loader {
id: content id: content
active: root.shouldBeActive || root.visible active: true
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
visible: true
sourceComponent: Content { sourceComponent: Content {
screen: root.screen screen: root.screen
+4 -32
View File
@@ -3,7 +3,6 @@ import QtQuick
import QtQuick.VectorImage import QtQuick.VectorImage
import Quickshell import Quickshell
import Quickshell.Services.SystemTray import Quickshell.Services.SystemTray
import qs.Helpers
import qs.Modules import qs.Modules
import qs.Components import qs.Components
import qs.Config import qs.Config
@@ -12,36 +11,12 @@ Item {
id: root id: root
property bool current: popouts.currentName.startsWith(`traymenu${ind}`) && popouts.hasCurrent property bool current: popouts.currentName.startsWith(`traymenu${ind}`) && popouts.hasCurrent
readonly property real dpr: Hypr.monitorFor(loader.screen).scale property bool hasLoaded: false
required property int ind required property int ind
required property SystemTrayItem item required property SystemTrayItem item
required property RowLayout loader required property RowLayout loader
required property Wrapper popouts required property Wrapper popouts
function resolveIcon(app: string, icon: string): string {
if (app === "chrome_status_icon_1") {
return Quickshell.iconPath("discord-tray");
} else if (app === "AyuGramDesktop") {
if (icon === Quickshell.iconPath("com.ayugram.desktop-attention-symbolic"))
return Quickshell.iconPath("telegram-attention-panel");
else if (icon === Quickshell.iconPath("com.ayugram.desktop-mute-symbolic"))
return Quickshell.iconPath("telegram-mute-panel");
else if (icon === Quickshell.iconPath("com.ayugram.desktop-symbolic"))
return Quickshell.iconPath("telegram-panel");
} else if (app === "TelegramDesktop") {
if (icon === Quickshell.iconPath("org.telegram.desktop-symbolic"))
return Quickshell.iconPath("telegram-panel");
else if (icon === Quickshell.iconPath("org.telegram.desktop-attention-symbolic"))
return Quickshell.iconPath("telegram-attention-panel");
else if (icon === Quickshell.iconPath("org.telegram.desktop-mute-symbolic"))
return Quickshell.iconPath("telegram-mute-panel");
} else if (app === "steam") {
return Quickshell.iconPath("steam_tray_mono");
}
return root.item.icon;
}
CustomRect { CustomRect {
anchors.fill: parent anchors.fill: parent
anchors.margins: 3 anchors.margins: 3
@@ -55,8 +30,7 @@ Item {
onClicked: { onClicked: {
if (mouse.button === Qt.LeftButton) { if (mouse.button === Qt.LeftButton) {
root.item.activate(); root.item.activate();
console.log(icon.source + "\n" + root.item.id); } else if (mouse.button === Qt.RightButton) {
} else if (mouse.button === Qt.RightButton && Config.barConfig.popouts.tray) {
root.popouts.currentName = `traymenu${root.ind}`; root.popouts.currentName = `traymenu${root.ind}`;
root.popouts.currentCenter = Qt.binding(() => root.mapToItem(root.loader, root.implicitWidth / 2, 0).x); root.popouts.currentCenter = Qt.binding(() => root.mapToItem(root.loader, root.implicitWidth / 2, 0).x);
root.popouts.hasCurrent = true; root.popouts.hasCurrent = true;
@@ -74,11 +48,9 @@ Item {
id: icon id: icon
anchors.centerIn: parent anchors.centerIn: parent
antialiasing: true
color: root.current ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface color: root.current ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface
implicitSize: Config.barConfig.tray.trayIconSize * root.dpr implicitSize: 22
layer.enabled: Config.general.color.smart || Config.general.color.scheduleDark layer.enabled: Config.general.color.smart || Config.general.color.scheduleDark
scale: 1 / root.dpr source: root.item.icon
source: root.resolveIcon(root.item.id, root.item.icon)
} }
} }
+2 -2
View File
@@ -23,12 +23,12 @@ RowLayout {
let modRowPos = sysTrayMod.mapToItem(sysModRow, modPos.x, modPos.y); let modRowPos = sysTrayMod.mapToItem(sysModRow, modPos.x, modPos.y);
let child = sysModRow.childAt(modRowPos.x, modRowPos.y); let child = sysModRow.childAt(modRowPos.x, modRowPos.y);
if (child) { if (child) {
if (child.objectName === "audioWidget" && Config.barConfig.popouts.audio) if (child.objectName === "audioWidget")
return { return {
id: "audio", id: "audio",
item: child item: child
}; };
if (child.objectName === "upowerWidget" && Config.barConfig.popouts.upower) if (child.objectName === "upowerWidget")
return { return {
id: "upower", id: "upower",
item: child item: child
+112 -123
View File
@@ -11,161 +11,150 @@ import qs.Helpers
CustomClippingRect { CustomClippingRect {
id: root id: root
readonly property bool hasUpdates: Object.keys(Updates.updates)?.length > 0
readonly property int itemHeight: 50 + Appearance.padding.smaller * 2 readonly property int itemHeight: 50 + Appearance.padding.smaller * 2
required property var wrapper required property var wrapper
color: DynamicColors.tPalette.m3surfaceContainer color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: hasUpdates ? updatesListLoader.item?.implicitHeight + Appearance.padding.small * 2 : noUpdatesLoader.item.height implicitHeight: updatesList.visible ? updatesList.implicitHeight + Appearance.padding.small * 2 : noUpdates.height
implicitWidth: hasUpdates ? updatesListLoader.item?.contentWidth + Appearance.padding.small * 2 : noUpdatesLoader.item.width implicitWidth: updatesList.visible ? updatesList.contentWidth + Appearance.padding.small * 2 : noUpdates.width
radius: Appearance.rounding.small radius: Appearance.rounding.small
Loader { Item {
id: noUpdatesLoader id: noUpdates
active: !root.hasUpdates
anchors.centerIn: parent anchors.centerIn: parent
height: 200
visible: script.values.length === 0
width: 600
sourceComponent: Item { MaterialIcon {
id: noUpdates id: noUpdatesIcon
height: 200 anchors.horizontalCenter: parent.horizontalCenter
width: 300 anchors.top: parent.top
color: DynamicColors.tPalette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.extraLarge * 3
horizontalAlignment: Text.AlignHCenter
text: "check"
}
MaterialIcon { CustomText {
id: noUpdatesIcon anchors.horizontalCenter: parent.horizontalCenter
anchors.top: noUpdatesIcon.bottom
anchors.horizontalCenter: parent.horizontalCenter color: DynamicColors.tPalette.m3onSurfaceVariant
anchors.top: parent.top horizontalAlignment: Text.AlignHCenter
color: DynamicColors.tPalette.m3onSurfaceVariant text: qsTr("No updates available")
font.pointSize: Appearance.font.size.extraLarge * 3 verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
text: "check"
}
CustomText {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: noUpdatesIcon.bottom
color: DynamicColors.tPalette.m3onSurfaceVariant
horizontalAlignment: Text.AlignHCenter
text: qsTr("No updates available")
verticalAlignment: Text.AlignVCenter
}
} }
} }
Loader { CustomListView {
id: updatesListLoader id: updatesList
active: root.hasUpdates
anchors.centerIn: parent anchors.centerIn: parent
contentHeight: childrenRect.height
contentWidth: 600
displayMarginBeginning: root.itemHeight
displayMarginEnd: root.itemHeight
implicitHeight: Math.min(contentHeight, (root.itemHeight + spacing) * 5 - spacing)
implicitWidth: contentWidth
spacing: Appearance.spacing.normal
visible: script.values.length > 0
sourceComponent: CustomListView { delegate: CustomRect {
id: updatesList id: update
contentHeight: childrenRect.height required property var modelData
contentWidth: 600 readonly property list<string> sections: modelData.update.split(" ")
displayMarginBeginning: root.itemHeight
displayMarginEnd: root.itemHeight
implicitHeight: Math.min(contentHeight, (root.itemHeight + spacing) * 5 - spacing)
implicitWidth: contentWidth
spacing: Appearance.spacing.normal
delegate: CustomRect { // anchors.left: parent.left
id: update // anchors.right: parent.right
color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: root.itemHeight
implicitWidth: 600
radius: Appearance.rounding.small - Appearance.padding.small
required property var modelData RowLayout {
readonly property list<string> sections: modelData.update.split(" ") anchors.fill: parent
anchors.leftMargin: Appearance.padding.smaller
anchors.rightMargin: Appearance.padding.smaller
// anchors.left: parent.left MaterialIcon {
// anchors.right: parent.right font.pointSize: Appearance.font.size.large * 2
color: DynamicColors.tPalette.m3surfaceContainer text: "package_2"
implicitHeight: root.itemHeight }
implicitWidth: 600
radius: Appearance.rounding.small - Appearance.padding.small ColumnLayout {
Layout.fillWidth: true
CustomText {
Layout.fillWidth: true
Layout.preferredHeight: 25
elide: Text.ElideRight
font.pointSize: Appearance.font.size.large
text: update.sections[0]
}
CustomText {
Layout.fillWidth: true
color: DynamicColors.palette.m3onSurfaceVariant
text: Updates.formatUpdateTime(update.modelData.timestamp)
}
}
RowLayout { RowLayout {
anchors.fill: parent Layout.fillHeight: true
anchors.leftMargin: Appearance.padding.smaller Layout.preferredWidth: 300
anchors.rightMargin: Appearance.padding.smaller
MarqueeText {
id: versionFrom
Layout.fillHeight: true
Layout.preferredWidth: 125
animate: true
color: DynamicColors.palette.m3tertiary
font.pointSize: Appearance.font.size.large
horizontalAlignment: Text.AlignHCenter
marqueeEnabled: true
pauseMs: 4000
text: update.sections[1]
width: 125
}
MaterialIcon { MaterialIcon {
font.pointSize: Appearance.font.size.large * 2
text: "package_2"
}
ColumnLayout {
Layout.fillWidth: true
CustomText {
Layout.fillWidth: true
Layout.preferredHeight: 25
elide: Text.ElideRight
font.pointSize: Appearance.font.size.large
text: update.sections[0]
}
CustomText {
Layout.fillWidth: true
color: DynamicColors.palette.m3onSurfaceVariant
text: Updates.formatUpdateTime(update.modelData.timestamp)
}
}
RowLayout {
Layout.fillHeight: true Layout.fillHeight: true
Layout.preferredWidth: 300 color: DynamicColors.palette.m3secondary
font.pointSize: Appearance.font.size.extraLarge
horizontalAlignment: Text.AlignHCenter
text: "arrow_right_alt"
verticalAlignment: Text.AlignVCenter
}
MarqueeText { MarqueeText {
id: versionFrom id: versionTo
Layout.fillHeight: true Layout.fillHeight: true
Layout.preferredWidth: 125 Layout.preferredWidth: 120
animate: true animate: true
color: DynamicColors.palette.m3tertiary color: DynamicColors.palette.m3primary
font.pointSize: Appearance.font.size.large font.pointSize: Appearance.font.size.large
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
marqueeEnabled: true marqueeEnabled: true
pauseMs: 4000 pauseMs: 4000
text: update.sections[1] text: update.sections[3]
width: 125 width: 125
}
MaterialIcon {
Layout.fillHeight: true
color: DynamicColors.palette.m3secondary
font.pointSize: Appearance.font.size.extraLarge
horizontalAlignment: Text.AlignHCenter
text: "arrow_right_alt"
verticalAlignment: Text.AlignVCenter
}
MarqueeText {
id: versionTo
Layout.fillHeight: true
Layout.preferredWidth: 120
animate: true
color: DynamicColors.palette.m3primary
font.pointSize: Appearance.font.size.large
horizontalAlignment: Text.AlignHCenter
marqueeEnabled: true
pauseMs: 4000
text: update.sections[3]
width: 125
}
} }
} }
} }
model: ScriptModel { }
id: script model: ScriptModel {
id: script
objectProp: "update" objectProp: "update"
values: Object.entries(Updates.updates).sort((a, b) => b[1] - a[1]).map(([update, timestamp]) => ({ values: Object.entries(Updates.updates).sort((a, b) => b[1] - a[1]).map(([update, timestamp]) => ({
update, update,
timestamp timestamp
})) }))
}
} }
} }
} }
+80
View File
@@ -0,0 +1,80 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Components
import qs.Helpers
import qs.Config
Item {
id: root
property Image current: one
property string source: Wallpapers.current
anchors.fill: parent
Component.onCompleted: {
if (source)
Qt.callLater(() => one.update());
}
onSourceChanged: {
if (!source) {
current = null;
} else if (current === one) {
two.update();
} else {
one.update();
}
}
Img {
id: one
}
Img {
id: two
}
component Img: Image {
id: img
function update(): void {
if (source === root.source) {
root.current = this;
} else {
source = root.source;
}
}
anchors.fill: parent
asynchronous: true
fillMode: Image.PreserveAspectCrop
opacity: 0
retainWhileLoading: true
scale: Wallpapers.showPreview ? 1 : 0.8
sourceClipRect: Qt.rect(Config.background.sourceClipX, Config.background.sourceClipY, Config.background.sourceClipW, Config.background.sourceClipH)
states: State {
name: "visible"
when: root.current === img
PropertyChanges {
img.opacity: 1
img.scale: 1
}
}
transitions: Transition {
Anim {
duration: Config.background.wallFadeDuration
properties: "opacity,scale"
target: img
}
}
onStatusChanged: {
if (status === Image.Ready) {
root.current = this;
}
}
}
}
+31 -38
View File
@@ -6,7 +6,6 @@ import QtQuick
import qs.Components import qs.Components
import qs.Helpers import qs.Helpers
import qs.Config import qs.Config
import ZShell.Internal
Item { Item {
id: root id: root
@@ -16,64 +15,58 @@ Item {
function refreshData(): void { function refreshData(): void {
Hyprland.refreshMonitors(); Hyprland.refreshMonitors();
let scale = Hyprland.monitorFor(root.screen).scale; const scale = Hyprland.monitorFor(root.screen).scale;
if (scale <= 0) if (scale > 0 && img.resScale !== scale) {
scale = 1.0; // Fallback to avoid zeroes on initialization img.resScale = scale;
img.sourceSize.width = root.screen.width * scale;
if (root.screen.width > 0 && root.screen.height > 0) {
img.screenResolution = Qt.size(root.screen.width * scale, root.screen.height * scale);
} }
const displayData = Wallpapers.getCrop(root.screen.name); const displayData = Wallpapers.getCrop(root.screen.name);
const displayRect = Qt.rect(img.sourceSize.width * displayData.x, img.implicitHeight * displayData.y, img.sourceSize.width * displayData.width, img.implicitHeight * displayData.height);
if (displayData) { img.anchors.fill = null;
img.cropX = displayData.x !== undefined ? displayData.x : 0.0; img.zoom = displayData.zoom;
img.cropY = displayData.y !== undefined ? displayData.y : 0.0; img.x = -(displayRect.x * displayData.zoom / img.resScale);
img.cropWidth = (displayData.width !== undefined && displayData.width > 0) ? displayData.width : 1.0; img.y = -(displayRect.y * displayData.zoom / img.resScale);
img.cropHeight = (displayData.height !== undefined && displayData.height > 0) ? displayData.height : 1.0;
}
} }
anchors.fill: parent anchors.fill: parent
Component.onCompleted: root.refreshData() Image {
Connections {
function onHeightChanged() {
root.refreshData();
}
function onWidthChanged() {
root.refreshData();
}
target: root.screen
}
WallpaperImage {
id: img id: img
anchors.fill: parent property int displayH
source: root.source property int displayW
property real resScale
property real zoom: 1.0
Behavior on cropHeight { asynchronous: true
fillMode: Image.PreserveAspectCrop
height: implicitHeight * zoom / resScale
opacity: 1
retainWhileLoading: true
source: root.source
sourceSize.width: root.screen.width * resScale
width: implicitWidth * zoom / resScale
Behavior on height {
Anim { Anim {
} }
} }
Behavior on cropWidth { Behavior on width {
Anim { Anim {
} }
} }
Behavior on cropX { Behavior on x {
Anim { Anim {
} }
} }
Behavior on cropY { Behavior on y {
Anim { Anim {
} }
} }
Behavior on zoom {
Anim { onStatusChanged: {
if (img.status == Image.Ready) {
root.refreshData();
} }
} }
+1 -1
View File
@@ -6,7 +6,7 @@ import qs.Modules.DesktopIcons
Loader { Loader {
active: Config.background.enabled active: Config.background.enabled
asynchronous: false asynchronous: true
sourceComponent: Variants { sourceComponent: Variants {
model: Quickshell.screens model: Quickshell.screens
+49 -4
View File
@@ -15,8 +15,9 @@ Item {
property real currentCenter property real currentCenter
property alias currentName: popoutState.currentName property alias currentName: popoutState.currentName
property string detachedMode property string detachedMode
readonly property bool isDetached: detachedMode.length > 0
property alias hasCurrent: popoutState.hasCurrent property alias hasCurrent: popoutState.hasCurrent
readonly property real nonAnimHeight: content.implicitHeight || 150 readonly property real nonAnimHeight: children.find(c => c.shouldBeActive)?.implicitHeight ?? content.implicitHeight
readonly property real nonAnimWidth: children.find(c => c.shouldBeActive)?.implicitWidth ?? content.implicitWidth readonly property real nonAnimWidth: children.find(c => c.shouldBeActive)?.implicitWidth ?? content.implicitWidth
required property real offsetScale required property real offsetScale
property string queuedMode property string queuedMode
@@ -27,13 +28,29 @@ Item {
detachedMode = ""; detachedMode = "";
} }
function detach(mode: string): void {
setAnims(true);
if (mode === "winfo") {
detachedMode = mode;
} else {
queuedMode = mode;
detachedMode = "any";
}
setAnims(false);
focus = true;
}
function setAnims(detach: bool): void {
const type = `expressive${detach ? "Slow" : "Default"}Spatial`;
animLength = Appearance.anim.durations[type];
animCurve = Appearance.anim.curves[type];
}
focus: hasCurrent focus: hasCurrent
implicitHeight: nonAnimHeight implicitHeight: nonAnimHeight
implicitWidth: nonAnimWidth implicitWidth: nonAnimWidth
Behavior on implicitHeight { Behavior on implicitHeight {
enabled: root.offsetScale < 1
Anim { Anim {
duration: root.animLength duration: root.animLength
easing.bezierCurve: root.animCurve easing.bezierCurve: root.animCurve
@@ -55,14 +72,42 @@ Item {
Comp { Comp {
id: content id: content
// anchors.horizontalCenter: parent.horizontalCenter
// anchors.top: parent.top
anchors.centerIn: parent anchors.centerIn: parent
shouldBeActive: root.hasCurrent shouldBeActive: root.hasCurrent && !root.detachedMode
sourceComponent: Content { sourceComponent: Content {
popouts: popoutState popouts: popoutState
} }
} }
// Comp {
// id: winfo
//
// anchors.centerIn: parent
// shouldBeActive: root.detachedMode === "winfo"
//
// sourceComponent: WindowInfo {
// client: Hypr.activeToplevel
// screen: root.screen
// }
// }
//
// Comp {
// id: controlCenter
//
// anchors.centerIn: parent
// shouldBeActive: root.detachedMode === "any"
//
// sourceComponent: ControlCenter {
// active: root.queuedMode
// screen: root.screen
//
// onClose: root.close()
// }
// }
component Comp: Loader { component Comp: Loader {
id: comp id: comp
-1
View File
@@ -8,7 +8,6 @@ qml_module(ZShell-internal
circularbuffer.hpp circularbuffer.cpp circularbuffer.hpp circularbuffer.cpp
sparklineitem.hpp sparklineitem.cpp sparklineitem.hpp sparklineitem.cpp
arcgauge.hpp arcgauge.cpp arcgauge.hpp arcgauge.cpp
wallpaperimage.hpp wallpaperimage.cpp
LIBRARIES LIBRARIES
Qt::Gui Qt::Gui
Qt::Quick Qt::Quick
+2 -2
View File
@@ -4,7 +4,7 @@
#include <qpainter.h> #include <qpainter.h>
#include <qpen.h> #include <qpen.h>
namespace ZShell::internal { namespace caelestia::internal {
ArcGauge::ArcGauge(QQuickItem* parent) ArcGauge::ArcGauge(QQuickItem* parent)
: QQuickPaintedItem(parent) { : QQuickPaintedItem(parent) {
@@ -116,4 +116,4 @@ void ArcGauge::setLineWidth(qreal width) {
update(); update();
} }
} // namespace ZShell::internal } // namespace caelestia::internal
+2 -2
View File
@@ -5,7 +5,7 @@
#include <qqmlintegration.h> #include <qqmlintegration.h>
#include <qquickpainteditem.h> #include <qquickpainteditem.h>
namespace ZShell::internal { namespace caelestia::internal {
class ArcGauge : public QQuickPaintedItem { class ArcGauge : public QQuickPaintedItem {
Q_OBJECT Q_OBJECT
@@ -58,4 +58,4 @@ qreal m_sweepAngle = 1.5 * M_PI;
qreal m_lineWidth = 10.0; qreal m_lineWidth = 10.0;
}; };
} // namespace ZShell::internal } // namespace caelestia::internal
+19 -140
View File
@@ -1,18 +1,11 @@
#include "hyprextras.hpp" #include "hyprextras.hpp"
#include "hyprdevices.hpp" #include "hyprdevices.hpp"
#include <functional>
#include <memory>
#include <qdir.h> #include <qdir.h>
#include <qcolor.h>
#include <qjsonarray.h> #include <qjsonarray.h>
#include <qjsondocument.h>
#include <qjsonobject.h>
#include <qlocalsocket.h> #include <qlocalsocket.h>
#include <qloggingcategory.h> #include <qloggingcategory.h>
#include <qmetatype.h> #include <qmetatype.h>
#include <qobject.h>
#include <qregularexpression.h> #include <qregularexpression.h>
#include <qvariant.h> #include <qvariant.h>
@@ -170,86 +163,6 @@ static QString buildHlConfigCall(const QString& key, const QVariant& value) {
return out; return out;
} }
static QColor colorFromInt(quint32 value) {
const int a = (value >> 24) & 0xFF;
const int r = (value >> 16) & 0xFF;
const int g = (value >> 8) & 0xFF;
const int b = value & 0xFF;
return QColor(r, g, b, a);
}
static QVariant parseGetOptionValue(const QJsonObject& obj) {
if (obj.contains(QStringLiteral("bool"))) {
return obj.value(QStringLiteral("bool")).toBool();
}
if (obj.contains(QStringLiteral("int"))) {
const auto value = obj.value(QStringLiteral("int")).toInt();
const auto option = obj.value(QStringLiteral("option")).toString();
if (option.contains(QStringLiteral("color")) || option.contains(QStringLiteral("col."))) {
return colorFromInt(static_cast<quint32>(value));
}
return value;
}
if (obj.contains(QStringLiteral("float"))) {
return obj.value(QStringLiteral("float")).toDouble();
}
if (obj.contains(QStringLiteral("str"))) {
return obj.value(QStringLiteral("str")).toString();
}
if (obj.contains(QStringLiteral("current"))) {
return obj.value(QStringLiteral("current")).toVariant();
}
if (obj.contains(QStringLiteral("value"))) {
return obj.value(QStringLiteral("value")).toVariant();
}
if (obj.contains(QStringLiteral("vec2"))) {
return obj.value(QStringLiteral("vec2")).toVariant();
}
if (obj.contains(QStringLiteral("data"))) {
const auto data = obj.value(QStringLiteral("data"));
if (data.isObject()) {
const auto d = data.toObject();
if (d.contains(QStringLiteral("current"))) {
return d.value(QStringLiteral("current")).toVariant();
}
if (d.contains(QStringLiteral("value"))) {
return d.value(QStringLiteral("value")).toVariant();
}
} else {
return data.toVariant();
}
}
return {};
}
static void insertNestedValue(QVariantMap& root, const QStringList& path, const QVariant& value) {
if (path.isEmpty()) {
return;
}
if (path.size() == 1) {
root.insert(path.first(), value);
return;
}
const QString head = path.first();
QVariantMap child = root.value(head).toMap();
insertNestedValue(child, path.mid(1), value);
root.insert(head, child);
}
} // namespace } // namespace
HyprExtras::HyprExtras(QObject* parent) HyprExtras::HyprExtras(QObject* parent)
@@ -290,7 +203,7 @@ HyprExtras::HyprExtras(QObject* parent)
m_socket->connectToServer(m_eventSocket, QLocalSocket::ReadOnly); m_socket->connectToServer(m_eventSocket, QLocalSocket::ReadOnly);
} }
QVariantMap HyprExtras::options() const { QVariantHash HyprExtras::options() const {
return m_options; return m_options;
} }
@@ -356,64 +269,30 @@ void HyprExtras::refreshOptions() {
m_optionsRefresh->close(); m_optionsRefresh->close();
} }
++m_optionsRefreshGeneration; m_optionsRefresh = makeRequestJson(QStringLiteral("descriptions"), [this](bool success, const QJsonDocument& response) {
const quint64 generation = m_optionsRefreshGeneration; m_optionsRefresh.reset();
if (!success) {
static const QStringList optionKeys = {
QStringLiteral("general:border_size"),
QStringLiteral("decoration:rounding"),
QStringLiteral("animations:enabled"),
QStringLiteral("decoration:shadow:enabled"),
QStringLiteral("decoration:shadow:offset"),
QStringLiteral("decoration:shadow:color"),
QStringLiteral("decoration:shadow:range"),
QStringLiteral("decoration:shadow:render_power"),
};
auto nextOptions = std::make_shared<QVariantMap>();
auto step = std::make_shared<std::function<void(int)> >();
*step = [this, generation, nextOptions, step](int index) {
if (generation != m_optionsRefreshGeneration) {
return; return;
} }
if (index >= optionKeys.size()) { const auto options = response.array();
if (m_options != *nextOptions) { bool dirty = false;
m_options = *nextOptions;
emit optionsChanged(); for (const auto& o : std::as_const(options)) {
const auto obj = o.toObject();
const auto key = obj.value(QStringLiteral("value")).toString();
const auto value = obj.value(QStringLiteral("data")).toObject().value(QStringLiteral("current")).toVariant();
if (m_options.value(key) != value) {
dirty = true;
m_options.insert(key, value);
} }
return;
} }
const QString key = optionKeys.at(index); if (dirty) {
emit optionsChanged();
m_optionsRefresh = makeRequestJson( }
QStringLiteral("getoption ") + key, });
[this, generation, nextOptions, step, index, key](bool success, const QJsonDocument& response)
{
m_optionsRefresh.reset();
if (generation != m_optionsRefreshGeneration) {
return;
}
if (success && response.isObject()) {
const QVariant value = parseGetOptionValue(response.object());
if (value.isValid()) {
insertNestedValue(*nextOptions, key.split(QLatin1Char(':'), Qt::SkipEmptyParts), value);
} else {
qCWarning(lcHypr) << "refreshOptions: getoption returned no usable value for" << key;
}
} else if (!success) {
qCWarning(lcHypr) << "refreshOptions: getoption request error for" << key;
}
(*step)(index + 1);
});
};
(*step)(0);
} }
void HyprExtras::refreshDevices() { void HyprExtras::refreshDevices() {
+3 -8
View File
@@ -1,13 +1,9 @@
#pragma once #pragma once
#include <functional>
#include <qjsondocument.h>
#include <qlocalsocket.h> #include <qlocalsocket.h>
#include <qobject.h> #include <qobject.h>
#include <qqmlintegration.h> #include <qqmlintegration.h>
#include <qsharedpointer.h> #include <qsharedpointer.h>
#include <qstringlist.h>
#include <qvariant.h> #include <qvariant.h>
namespace ZShell::internal::hypr { namespace ZShell::internal::hypr {
@@ -19,13 +15,13 @@ Q_OBJECT
QML_ELEMENT QML_ELEMENT
Q_MOC_INCLUDE("hyprdevices.hpp") Q_MOC_INCLUDE("hyprdevices.hpp")
Q_PROPERTY(QVariantMap options READ options NOTIFY optionsChanged) Q_PROPERTY(QVariantHash options READ options NOTIFY optionsChanged)
Q_PROPERTY(ZShell::internal::hypr::HyprDevices* devices READ devices CONSTANT) Q_PROPERTY(ZShell::internal::hypr::HyprDevices* devices READ devices CONSTANT)
public: public:
explicit HyprExtras(QObject* parent = nullptr); explicit HyprExtras(QObject* parent = nullptr);
[[nodiscard]] QVariantMap options() const; [[nodiscard]] QVariantHash options() const;
[[nodiscard]] HyprDevices* devices() const; [[nodiscard]] HyprDevices* devices() const;
Q_INVOKABLE void message(const QString& message); Q_INVOKABLE void message(const QString& message);
@@ -46,12 +42,11 @@ QString m_eventSocket;
QLocalSocket* m_socket; QLocalSocket* m_socket;
bool m_socketValid; bool m_socketValid;
QVariantMap m_options; QVariantHash m_options;
HyprDevices* const m_devices; HyprDevices* const m_devices;
SocketPtr m_optionsRefresh; SocketPtr m_optionsRefresh;
SocketPtr m_devicesRefresh; SocketPtr m_devicesRefresh;
quint64 m_optionsRefreshGeneration = 0;
void socketError(QLocalSocket::LocalSocketError error) const; void socketError(QLocalSocket::LocalSocketError error) const;
void socketStateChanged(QLocalSocket::LocalSocketState state); void socketStateChanged(QLocalSocket::LocalSocketState state);
-199
View File
@@ -1,199 +0,0 @@
#include "wallpaperimage.hpp"
#include <QStandardPaths>
#include <QCryptographicHash>
#include <QDir>
#include <QFileInfo>
#include <QtConcurrent>
#include <QSGImageNode>
#include <QQuickWindow>
namespace ZShell::internal {
WallpaperImage::WallpaperImage(QQuickItem *parent)
: QQuickItem(parent)
{
setFlag(ItemHasContents, true);
connect(&m_imageWatcher, &QFutureWatcher<QImage>::finished, this, &WallpaperImage::handleImageLoaded);
}
WallpaperImage::~WallpaperImage() {
if (m_texture) delete m_texture;
}
void WallpaperImage::setSource(const QUrl &source) {
if (m_source == source) return;
m_source = source;
emit sourceChanged();
loadImage();
}
void WallpaperImage::setScreenResolution(const QSize &screenResolution) {
if (m_screenResolution == screenResolution) return;
m_screenResolution = screenResolution;
emit screenResolutionChanged();
loadImage();
}
void WallpaperImage::setZoom(qreal zoom) {
if (qFuzzyCompare(m_zoom, zoom)) return;
m_zoom = zoom;
emit zoomChanged();
update();
}
void WallpaperImage::setCropX(qreal x) {
if (qFuzzyCompare(m_cropX, x)) return;
m_cropX = x;
emit cropXChanged();
update();
}
void WallpaperImage::setCropY(qreal y) {
if (qFuzzyCompare(m_cropY, y)) return;
m_cropY = y;
emit cropYChanged();
update();
}
void WallpaperImage::setCropWidth(qreal w) {
if (w <= 0.0) w = 1.0;
if (qFuzzyCompare(m_cropWidth, w)) return;
m_cropWidth = w;
emit cropWidthChanged();
update();
}
void WallpaperImage::setCropHeight(qreal h) {
if (h <= 0.0) h = 1.0;
if (qFuzzyCompare(m_cropHeight, h)) return;
m_cropHeight = h;
emit cropHeightChanged();
update();
}
QString WallpaperImage::getCacheFilePath() const {
if (m_source.isEmpty() || m_screenResolution.isEmpty()) return QString();
QString cachePath = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + "/zshell/imagecache";
QDir().mkpath(cachePath);
// Hash the source URL + resolution
QString id = m_source.toString() + "_" + QString::number(m_screenResolution.width()) + "x" + QString::number(m_screenResolution.height());
QByteArray hash = QCryptographicHash::hash(id.toUtf8(), QCryptographicHash::Md5).toHex();
return cachePath + "/" + hash + ".png";
}
void WallpaperImage::loadImage() {
if (m_source.isEmpty()) return;
QString cacheFile = getCacheFilePath();
QString sourceFile = m_source.isLocalFile() ? m_source.toLocalFile() : m_source.toString();
// Qt resource path correction if passed as a standard URL string
if (sourceFile.startsWith("qrc:/")) {
sourceFile = sourceFile.mid(3); // Converts "qrc:/" to ":/"
}
QSize targetRes = m_screenResolution;
// Run off the main thread to avoid blocking the UI
QFuture<QImage> future = QtConcurrent::run([sourceFile, cacheFile, targetRes]() -> QImage {
if (!targetRes.isEmpty() && !cacheFile.isEmpty() && QFileInfo::exists(cacheFile)) {
QImage cached(cacheFile);
if (!cached.isNull()) return cached;
}
QImage original(sourceFile);
if (original.isNull()) return QImage();
if (targetRes.isEmpty()) {
// Screen resolution not set yet by QML, return the unscaled original for now to prevent a black screen
return original;
}
// Check if original is strictly larger than screen resolution
if (original.width() > targetRes.width() || original.height() > targetRes.height()) {
QImage scaled = original.scaled(targetRes, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
if (!cacheFile.isEmpty()) scaled.save(cacheFile, "PNG");
return scaled;
}
// Otherwise just cache and return the original
if (!cacheFile.isEmpty()) original.save(cacheFile, "PNG");
return original;
});
m_imageWatcher.setFuture(future);
}
void WallpaperImage::handleImageLoaded() {
m_image = m_imageWatcher.result();
m_textureDirty = true;
update(); // Request redraw
}
QSGNode *WallpaperImage::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) {
auto *node = static_cast<QSGImageNode *>(oldNode);
if (m_image.isNull()) {
delete node;
return nullptr;
}
if (!node) {
node = window()->createImageNode();
}
if (m_textureDirty) {
if (m_texture) delete m_texture;
m_texture = window()->createTextureFromImage(m_image, QQuickWindow::TextureHasAlphaChannel);
m_textureDirty = false;
}
if (m_texture) {
node->setTexture(m_texture);
node->setRect(boundingRect());
node->setFiltering(QSGTexture::Linear);
qreal cW = m_cropWidth / m_zoom;
qreal cH = m_cropHeight / m_zoom;
QRectF reqRect(
m_cropX * m_texture->textureSize().width(),
m_cropY * m_texture->textureSize().height(),
cW * m_texture->textureSize().width(),
cH * m_texture->textureSize().height()
);
QRectF bounds = boundingRect();
if (bounds.isEmpty() || reqRect.isEmpty()) return node;
qreal targetRatio = bounds.width() / bounds.height();
qreal reqRatio = reqRect.width() / reqRect.height();
QRectF sourceRect = reqRect;
// Force 'PreserveAspectCrop' behavior on the requested region
if (reqRatio > targetRatio) {
// Requested region is too wide, center-crop the sides
qreal newWidth = reqRect.height() * targetRatio;
qreal xOffset = (reqRect.width() - newWidth) / 2.0;
sourceRect.setX(reqRect.x() + xOffset);
sourceRect.setWidth(newWidth);
} else if (reqRatio < targetRatio) {
// Requested region is too tall, center-crop the top/bottom
qreal newHeight = reqRect.width() / targetRatio;
qreal yOffset = (reqRect.height() - newHeight) / 2.0;
sourceRect.setY(reqRect.y() + yOffset);
sourceRect.setHeight(newHeight);
}
node->setSourceRect(sourceRect);
}
return node;
}
} // namespace ZShell::internal
@@ -1,95 +0,0 @@
#pragma once
#include <QQuickItem>
#include <QImage>
#include <QUrl>
#include <QSGTexture>
#include <QFutureWatcher>
#include <QtQml/qqml.h>
namespace ZShell::internal {
class WallpaperImage : public QQuickItem {
Q_OBJECT
QML_NAMED_ELEMENT(WallpaperImage)
Q_PROPERTY(QUrl source READ source WRITE setSource NOTIFY sourceChanged)
Q_PROPERTY(QSize screenResolution READ screenResolution WRITE setScreenResolution NOTIFY screenResolutionChanged)
Q_PROPERTY(qreal zoom READ zoom WRITE setZoom NOTIFY zoomChanged)
Q_PROPERTY(qreal cropX READ cropX WRITE setCropX NOTIFY cropXChanged)
Q_PROPERTY(qreal cropY READ cropY WRITE setCropY NOTIFY cropYChanged)
Q_PROPERTY(qreal cropWidth READ cropWidth WRITE setCropWidth NOTIFY cropWidthChanged)
Q_PROPERTY(qreal cropHeight READ cropHeight WRITE setCropHeight NOTIFY cropHeightChanged)
public:
explicit WallpaperImage(QQuickItem *parent = nullptr);
~WallpaperImage() override;
QUrl source() const {
return m_source;
}
void setSource(const QUrl &source);
QSize screenResolution() const {
return m_screenResolution;
}
void setScreenResolution(const QSize &screenResolution);
qreal zoom() const {
return m_zoom;
}
void setZoom(qreal zoom);
qreal cropX() const {
return m_cropX;
}
void setCropX(qreal x);
qreal cropY() const {
return m_cropY;
}
void setCropY(qreal y);
qreal cropWidth() const {
return m_cropWidth;
}
void setCropWidth(qreal w);
qreal cropHeight() const {
return m_cropHeight;
}
void setCropHeight(qreal h);
protected:
QSGNode *updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *updatePaintNodeData) override;
signals:
void sourceChanged();
void screenResolutionChanged();
void zoomChanged();
void cropXChanged();
void cropYChanged();
void cropWidthChanged();
void cropHeightChanged();
private:
void loadImage();
void handleImageLoaded();
QString getCacheFilePath() const;
QUrl m_source;
QSize m_screenResolution;
qreal m_zoom = 1.0;
qreal m_cropX = 0.0;
qreal m_cropY = 0.0;
qreal m_cropWidth = 1.0;
qreal m_cropHeight = 1.0;
QImage m_image;
QSGTexture *m_texture = nullptr;
bool m_textureDirty = false;
QFutureWatcher<QImage> m_imageWatcher;
};
} // namespace ZShell::internal
+302 -311
View File
@@ -7,473 +7,464 @@
namespace ZShell::models { namespace ZShell::models {
FileSystemEntry::FileSystemEntry(const QString& path, const QString& relativePath, QObject* parent) FileSystemEntry::FileSystemEntry(const QString& path, const QString& relativePath, QObject* parent)
: QObject(parent) : QObject(parent)
, m_fileInfo(path) , m_fileInfo(path)
, m_path(path) , m_path(path)
, m_relativePath(relativePath) , m_relativePath(relativePath)
, m_isImageInitialised(false) , m_isImageInitialised(false)
, m_mimeTypeInitialised(false) {} , m_mimeTypeInitialised(false) {
}
QString FileSystemEntry::path() const { QString FileSystemEntry::path() const {
return m_path; return m_path;
}; };
QString FileSystemEntry::relativePath() const { QString FileSystemEntry::relativePath() const {
return m_relativePath; return m_relativePath;
}; };
QString FileSystemEntry::name() const { QString FileSystemEntry::name() const {
return m_fileInfo.fileName(); return m_fileInfo.fileName();
}; };
QString FileSystemEntry::baseName() const { QString FileSystemEntry::baseName() const {
return m_fileInfo.baseName(); return m_fileInfo.baseName();
}; };
QString FileSystemEntry::parentDir() const { QString FileSystemEntry::parentDir() const {
return m_fileInfo.absolutePath(); return m_fileInfo.absolutePath();
}; };
QString FileSystemEntry::suffix() const { QString FileSystemEntry::suffix() const {
return m_fileInfo.completeSuffix(); return m_fileInfo.completeSuffix();
}; };
qint64 FileSystemEntry::size() const { qint64 FileSystemEntry::size() const {
return m_fileInfo.size(); return m_fileInfo.size();
}; };
bool FileSystemEntry::isDir() const { bool FileSystemEntry::isDir() const {
return m_fileInfo.isDir(); return m_fileInfo.isDir();
}; };
bool FileSystemEntry::isImage() const { bool FileSystemEntry::isImage() const {
if (!m_isImageInitialised) { if (!m_isImageInitialised) {
QImageReader reader(m_path); QImageReader reader(m_path);
m_isImage = reader.canRead(); m_isImage = reader.canRead();
m_isImageInitialised = true; m_isImageInitialised = true;
} }
return m_isImage; return m_isImage;
} }
QString FileSystemEntry::mimeType() const { QString FileSystemEntry::mimeType() const {
if (!m_mimeTypeInitialised) { if (!m_mimeTypeInitialised) {
const QMimeDatabase db; static const QMimeDatabase s_db;
m_mimeType = db.mimeTypeForFile(m_path).name(); m_mimeType = s_db.mimeTypeForFile(m_path).name();
m_mimeTypeInitialised = true; m_mimeTypeInitialised = true;
} }
return m_mimeType; return m_mimeType;
} }
void FileSystemEntry::updateRelativePath(const QDir& dir) { void FileSystemEntry::updateRelativePath(const QDir& dir) {
const auto relPath = dir.relativeFilePath(m_path); const auto relPath = dir.relativeFilePath(m_path);
if (m_relativePath != relPath) { if (m_relativePath != relPath) {
m_relativePath = relPath; m_relativePath = relPath;
emit relativePathChanged(); emit relativePathChanged();
} }
} }
FileSystemModel::FileSystemModel(QObject* parent) FileSystemModel::FileSystemModel(QObject* parent)
: QAbstractListModel(parent) : QAbstractListModel(parent)
, m_recursive(false) , m_recursive(false)
, m_watchChanges(true) , m_watchChanges(true)
, m_showHidden(false) , m_showHidden(false)
, m_filter(NoFilter) { , m_filter(NoFilter) {
connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &FileSystemModel::watchDirIfRecursive); connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &FileSystemModel::watchDirIfRecursive);
connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &FileSystemModel::updateEntriesForDir); connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &FileSystemModel::updateEntriesForDir);
} }
int FileSystemModel::rowCount(const QModelIndex& parent) const { int FileSystemModel::rowCount(const QModelIndex& parent) const {
if (parent != QModelIndex()) { if (parent != QModelIndex()) {
return 0; return 0;
} }
return static_cast<int>(m_entries.size()); return static_cast<int>(m_entries.size());
} }
QVariant FileSystemModel::data(const QModelIndex& index, int role) const { QVariant FileSystemModel::data(const QModelIndex& index, int role) const {
if (role != Qt::UserRole || !index.isValid() || index.row() >= m_entries.size()) { if (role != Qt::UserRole || !index.isValid() || index.row() >= m_entries.size()) {
return QVariant(); return QVariant();
} }
return QVariant::fromValue(m_entries.at(index.row())); return QVariant::fromValue(m_entries.at(index.row()));
} }
QHash<int, QByteArray> FileSystemModel::roleNames() const { QHash<int, QByteArray> FileSystemModel::roleNames() const {
return { { Qt::UserRole, "modelData" } }; return { { Qt::UserRole, "modelData" } };
} }
QString FileSystemModel::path() const { QString FileSystemModel::path() const {
return m_path; return m_path;
} }
void FileSystemModel::setPath(const QString& path) { void FileSystemModel::setPath(const QString& path) {
if (m_path == path) { if (m_path == path) {
return; return;
} }
m_path = path; m_path = path;
emit pathChanged(); emit pathChanged();
m_dir.setPath(m_path); m_dir.setPath(m_path);
for (const auto& entry : std::as_const(m_entries)) { for (const auto& entry : std::as_const(m_entries)) {
entry->updateRelativePath(m_dir); entry->updateRelativePath(m_dir);
} }
update(); update();
} }
bool FileSystemModel::recursive() const { bool FileSystemModel::recursive() const {
return m_recursive; return m_recursive;
} }
void FileSystemModel::setRecursive(bool recursive) { void FileSystemModel::setRecursive(bool recursive) {
if (m_recursive == recursive) { if (m_recursive == recursive) {
return; return;
} }
m_recursive = recursive; m_recursive = recursive;
emit recursiveChanged(); emit recursiveChanged();
update(); update();
} }
bool FileSystemModel::watchChanges() const { bool FileSystemModel::watchChanges() const {
return m_watchChanges; return m_watchChanges;
} }
void FileSystemModel::setWatchChanges(bool watchChanges) { void FileSystemModel::setWatchChanges(bool watchChanges) {
if (m_watchChanges == watchChanges) { if (m_watchChanges == watchChanges) {
return; return;
} }
m_watchChanges = watchChanges; m_watchChanges = watchChanges;
emit watchChangesChanged(); emit watchChangesChanged();
update(); update();
} }
bool FileSystemModel::showHidden() const { bool FileSystemModel::showHidden() const {
return m_showHidden; return m_showHidden;
} }
void FileSystemModel::setShowHidden(bool showHidden) { void FileSystemModel::setShowHidden(bool showHidden) {
if (m_showHidden == showHidden) { if (m_showHidden == showHidden) {
return; return;
} }
m_showHidden = showHidden; m_showHidden = showHidden;
emit showHiddenChanged(); emit showHiddenChanged();
update(); update();
} }
bool FileSystemModel::sortReverse() const { bool FileSystemModel::sortReverse() const {
return m_sortReverse; return m_sortReverse;
} }
void FileSystemModel::setSortReverse(bool sortReverse) { void FileSystemModel::setSortReverse(bool sortReverse) {
if (m_sortReverse == sortReverse) { if (m_sortReverse == sortReverse) {
return; return;
} }
m_sortReverse = sortReverse; m_sortReverse = sortReverse;
emit sortReverseChanged(); emit sortReverseChanged();
update(); update();
} }
FileSystemModel::Filter FileSystemModel::filter() const { FileSystemModel::Filter FileSystemModel::filter() const {
return m_filter; return m_filter;
} }
void FileSystemModel::setFilter(Filter filter) { void FileSystemModel::setFilter(Filter filter) {
if (m_filter == filter) { if (m_filter == filter) {
return; return;
} }
m_filter = filter; m_filter = filter;
emit filterChanged(); emit filterChanged();
update(); update();
} }
QStringList FileSystemModel::nameFilters() const { QStringList FileSystemModel::nameFilters() const {
return m_nameFilters; return m_nameFilters;
} }
void FileSystemModel::setNameFilters(const QStringList& nameFilters) { void FileSystemModel::setNameFilters(const QStringList& nameFilters) {
if (m_nameFilters == nameFilters) { if (m_nameFilters == nameFilters) {
return; return;
} }
m_nameFilters = nameFilters; m_nameFilters = nameFilters;
emit nameFiltersChanged(); emit nameFiltersChanged();
update(); update();
} }
QQmlListProperty<FileSystemEntry> FileSystemModel::entries() { QQmlListProperty<FileSystemEntry> FileSystemModel::entries() {
return QQmlListProperty<FileSystemEntry>(this, &m_entries); return QQmlListProperty<FileSystemEntry>(this, &m_entries);
} }
void FileSystemModel::watchDirIfRecursive(const QString& path) { void FileSystemModel::watchDirIfRecursive(const QString& path) {
if (m_recursive && m_watchChanges) { if (m_recursive && m_watchChanges) {
const auto currentDir = m_dir; const auto currentDir = m_dir;
const bool showHidden = m_showHidden; const bool showHidden = m_showHidden;
const auto future = QtConcurrent::run([showHidden, path]() { auto future = QtConcurrent::run([showHidden, path]() {
QDir::Filters filters = QDir::Dirs | QDir::NoDotAndDotDot; QDir::Filters filters = QDir::Dirs | QDir::NoDotAndDotDot;
if (showHidden) { if (showHidden) {
filters |= QDir::Hidden; filters |= QDir::Hidden;
} }
QDirIterator iter(path, filters, QDirIterator::Subdirectories); QDirIterator iter(path, filters, QDirIterator::Subdirectories);
QStringList dirs; QStringList dirs;
while (iter.hasNext()) { while (iter.hasNext()) {
dirs << iter.next(); dirs << iter.next();
} }
return dirs; return dirs;
}); });
const auto watcher = new QFutureWatcher<QStringList>(this); future.then(this, [currentDir, showHidden, this](const QStringList& paths) {
connect(watcher, &QFutureWatcher<QStringList>::finished, this, [currentDir, showHidden, watcher, this]() { if (currentDir == m_dir && showHidden == m_showHidden && !paths.isEmpty()) {
const auto paths = watcher->result(); // Ignore if dir or showHidden has changed
if (currentDir == m_dir && showHidden == m_showHidden && !paths.isEmpty()) { m_watcher.addPaths(paths);
// Ignore if dir or showHidden has changed }
m_watcher.addPaths(paths); });
} }
watcher->deleteLater();
});
watcher->setFuture(future);
}
} }
void FileSystemModel::update() { void FileSystemModel::update() {
updateWatcher(); updateWatcher();
updateEntries(); updateEntries();
} }
void FileSystemModel::updateWatcher() { void FileSystemModel::updateWatcher() {
if (!m_watcher.directories().isEmpty()) { if (!m_watcher.directories().isEmpty()) {
m_watcher.removePaths(m_watcher.directories()); m_watcher.removePaths(m_watcher.directories());
} }
if (!m_watchChanges || m_path.isEmpty()) { if (!m_watchChanges || m_path.isEmpty()) {
return; return;
} }
m_watcher.addPath(m_path); m_watcher.addPath(m_path);
watchDirIfRecursive(m_path); watchDirIfRecursive(m_path);
} }
void FileSystemModel::updateEntries() { void FileSystemModel::updateEntries() {
if (m_path.isEmpty()) { if (m_path.isEmpty()) {
if (!m_entries.isEmpty()) { if (!m_entries.isEmpty()) {
beginResetModel(); beginResetModel();
qDeleteAll(m_entries); qDeleteAll(m_entries);
m_entries.clear(); m_entries.clear();
endResetModel(); endResetModel();
emit entriesChanged(); emit entriesChanged();
} }
return; return;
} }
for (auto& future : m_futures) { for (auto& future : m_futures) {
future.cancel(); future.cancel();
} }
m_futures.clear(); m_futures.clear();
updateEntriesForDir(m_path); updateEntriesForDir(m_path);
} }
void FileSystemModel::updateEntriesForDir(const QString& dir) { void FileSystemModel::updateEntriesForDir(const QString& dir) {
const auto recursive = m_recursive; const auto recursive = m_recursive;
const auto showHidden = m_showHidden; const auto showHidden = m_showHidden;
const auto filter = m_filter; const auto filter = m_filter;
const auto nameFilters = m_nameFilters; const auto nameFilters = m_nameFilters;
QSet<QString> oldPaths; QSet<QString> oldPaths;
for (const auto& entry : std::as_const(m_entries)) { for (const auto& entry : std::as_const(m_entries)) {
oldPaths << entry->path(); oldPaths << entry->path();
} }
const auto future = QtConcurrent::run([=](QPromise<QPair<QSet<QString>, QSet<QString>>>& promise) { auto future = QtConcurrent::run([=](QPromise<QPair<QSet<QString>, QSet<QString> > >& promise) {
const auto flags = recursive ? QDirIterator::Subdirectories : QDirIterator::NoIteratorFlags; const auto flags = recursive ? QDirIterator::Subdirectories : QDirIterator::NoIteratorFlags;
std::optional<QDirIterator> iter; std::optional<QDirIterator> iter;
if (filter == Images) { if (filter == Images) {
QStringList extraNameFilters = nameFilters; QStringList extraNameFilters = nameFilters;
const auto formats = QImageReader::supportedImageFormats(); const auto formats = QImageReader::supportedImageFormats();
for (const auto& format : formats) { for (const auto& format : formats) {
extraNameFilters << "*." + format; extraNameFilters << "*." + format;
} }
QDir::Filters filters = QDir::Files; QDir::Filters filters = QDir::Files;
if (showHidden) { if (showHidden) {
filters |= QDir::Hidden; filters |= QDir::Hidden;
} }
iter.emplace(dir, extraNameFilters, filters, flags); iter.emplace(dir, extraNameFilters, filters, flags);
} else { } else {
QDir::Filters filters; QDir::Filters filters;
if (filter == Files) { if (filter == Files) {
filters = QDir::Files; filters = QDir::Files;
} else if (filter == Dirs) { } else if (filter == Dirs) {
filters = QDir::Dirs | QDir::NoDotAndDotDot; filters = QDir::Dirs | QDir::NoDotAndDotDot;
} else { } else {
filters = QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot; filters = QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot;
} }
if (showHidden) { if (showHidden) {
filters |= QDir::Hidden; filters |= QDir::Hidden;
} }
if (nameFilters.isEmpty()) { if (nameFilters.isEmpty()) {
iter.emplace(dir, filters, flags); iter.emplace(dir, filters, flags);
} else { } else {
iter.emplace(dir, nameFilters, filters, flags); iter.emplace(dir, nameFilters, filters, flags);
} }
} }
QSet<QString> newPaths; QSet<QString> newPaths;
while (iter->hasNext()) { while (iter->hasNext()) {
if (promise.isCanceled()) { if (promise.isCanceled()) {
return; return;
} }
QString path = iter->next(); QString path = iter->next();
if (filter == Images) { if (filter == Images) {
QImageReader reader(path); QImageReader reader(path);
if (!reader.canRead()) { if (!reader.canRead()) {
continue; continue;
} }
} }
newPaths.insert(path); newPaths.insert(path);
} }
if (promise.isCanceled() || newPaths == oldPaths) { if (promise.isCanceled()) {
return; return;
} }
promise.addResult(qMakePair(oldPaths - newPaths, newPaths - oldPaths)); promise.addResult(qMakePair(oldPaths - newPaths, newPaths - oldPaths));
}); });
if (m_futures.contains(dir)) { if (m_futures.contains(dir)) {
m_futures[dir].cancel(); m_futures[dir].cancel();
} }
m_futures.insert(dir, future); m_futures.insert(dir, future);
const auto watcher = new QFutureWatcher<QPair<QSet<QString>, QSet<QString>>>(this); future
.then(this,
connect(watcher, &QFutureWatcher<QPair<QSet<QString>, QSet<QString>>>::finished, this, [dir, watcher, this]() { [dir, this](QPair<QSet<QString>, QSet<QString> > result) {
m_futures.remove(dir); m_futures.remove(dir);
if (!result.first.isEmpty() || !result.second.isEmpty()) {
if (!watcher->future().isResultReadyAt(0)) { applyChanges(result.first, result.second);
watcher->deleteLater(); }
return; })
} .onCanceled(this, [dir, this]() {
m_futures.remove(dir);
const auto result = watcher->result(); });
applyChanges(result.first, result.second);
watcher->deleteLater();
});
watcher->setFuture(future);
} }
void FileSystemModel::applyChanges(const QSet<QString>& removedPaths, const QSet<QString>& addedPaths) { void FileSystemModel::applyChanges(const QSet<QString>& removedPaths, const QSet<QString>& addedPaths) {
QList<int> removedIndices; QList<int> removedIndices;
for (int i = 0; i < m_entries.size(); ++i) { for (int i = 0; i < m_entries.size(); ++i) {
if (removedPaths.contains(m_entries[i]->path())) { if (removedPaths.contains(m_entries[i]->path())) {
removedIndices << i; removedIndices << i;
} }
} }
std::sort(removedIndices.begin(), removedIndices.end(), std::greater<int>()); std::sort(removedIndices.begin(), removedIndices.end(), std::greater<int>());
// Batch remove old entries // Batch remove old entries
int start = -1; int start = -1;
int end = -1; int end = -1;
for (int idx : std::as_const(removedIndices)) { for (int idx : std::as_const(removedIndices)) {
if (start == -1) { if (start == -1) {
start = idx; start = idx;
end = idx; end = idx;
} else if (idx == end - 1) { } else if (idx == end - 1) {
end = idx; end = idx;
} else { } else {
beginRemoveRows(QModelIndex(), end, start); beginRemoveRows(QModelIndex(), end, start);
for (int i = start; i >= end; --i) { for (int i = start; i >= end; --i) {
m_entries.takeAt(i)->deleteLater(); m_entries.takeAt(i)->deleteLater();
} }
endRemoveRows(); endRemoveRows();
start = idx; start = idx;
end = idx; end = idx;
} }
} }
if (start != -1) { if (start != -1) {
beginRemoveRows(QModelIndex(), end, start); beginRemoveRows(QModelIndex(), end, start);
for (int i = start; i >= end; --i) { for (int i = start; i >= end; --i) {
m_entries.takeAt(i)->deleteLater(); m_entries.takeAt(i)->deleteLater();
} }
endRemoveRows(); endRemoveRows();
} }
// Create new entries // Create new entries
QList<FileSystemEntry*> newEntries; QList<FileSystemEntry*> newEntries;
for (const auto& path : addedPaths) { for (const auto& path : addedPaths) {
newEntries << new FileSystemEntry(path, m_dir.relativeFilePath(path), this); newEntries << new FileSystemEntry(path, m_dir.relativeFilePath(path), this);
} }
std::sort(newEntries.begin(), newEntries.end(), [this](const FileSystemEntry* a, const FileSystemEntry* b) { std::sort(newEntries.begin(), newEntries.end(), [this](const FileSystemEntry* a, const FileSystemEntry* b) {
return compareEntries(a, b); return compareEntries(a, b);
}); });
// Batch insert new entries // Batch insert new entries
int insertStart = -1; int insertStart = -1;
QList<FileSystemEntry*> batchItems; QList<FileSystemEntry*> batchItems;
for (const auto& entry : std::as_const(newEntries)) { for (const auto& entry : std::as_const(newEntries)) {
const auto it = std::lower_bound( const auto it = std::lower_bound(
m_entries.begin(), m_entries.end(), entry, [this](const FileSystemEntry* a, const FileSystemEntry* b) { m_entries.begin(), m_entries.end(), entry, [this](const FileSystemEntry* a, const FileSystemEntry* b) {
return compareEntries(a, b); return compareEntries(a, b);
}); });
const auto row = static_cast<int>(it - m_entries.begin()); const auto row = static_cast<int>(it - m_entries.begin());
if (insertStart == -1) { if (insertStart == -1) {
insertStart = row; insertStart = row;
batchItems << entry; batchItems << entry;
} else if (row == insertStart + batchItems.size()) { } else if (row == insertStart + batchItems.size()) {
batchItems << entry; batchItems << entry;
} else { } else {
beginInsertRows(QModelIndex(), insertStart, insertStart + static_cast<int>(batchItems.size()) - 1); beginInsertRows(QModelIndex(), insertStart, insertStart + static_cast<int>(batchItems.size()) - 1);
for (int i = 0; i < batchItems.size(); ++i) { for (int i = 0; i < batchItems.size(); ++i) {
m_entries.insert(insertStart + i, batchItems[i]); m_entries.insert(insertStart + i, batchItems[i]);
} }
endInsertRows(); endInsertRows();
insertStart = row; insertStart = row;
batchItems.clear(); batchItems.clear();
batchItems << entry; batchItems << entry;
} }
} }
if (!batchItems.isEmpty()) { if (!batchItems.isEmpty()) {
beginInsertRows(QModelIndex(), insertStart, insertStart + static_cast<int>(batchItems.size()) - 1); beginInsertRows(QModelIndex(), insertStart, insertStart + static_cast<int>(batchItems.size()) - 1);
for (int i = 0; i < batchItems.size(); ++i) { for (int i = 0; i < batchItems.size(); ++i) {
m_entries.insert(insertStart + i, batchItems[i]); m_entries.insert(insertStart + i, batchItems[i]);
} }
endInsertRows(); endInsertRows();
} }
emit entriesChanged(); emit entriesChanged();
} }
bool FileSystemModel::compareEntries(const FileSystemEntry* a, const FileSystemEntry* b) const { bool FileSystemModel::compareEntries(const FileSystemEntry* a, const FileSystemEntry* b) const {
if (a->isDir() != b->isDir()) { if (a->isDir() != b->isDir()) {
return m_sortReverse ^ a->isDir(); return m_sortReverse ^ a->isDir();
} }
const auto cmp = a->relativePath().localeAwareCompare(b->relativePath()); const auto cmp = a->relativePath().localeAwareCompare(b->relativePath());
return m_sortReverse ? cmp > 0 : cmp < 0; return m_sortReverse ? cmp > 0 : cmp < 0;
} }
} // namespace ZShell::models } // namespace ZShell::models
+95 -95
View File
@@ -13,136 +13,136 @@
namespace ZShell::models { namespace ZShell::models {
class FileSystemEntry : public QObject { class FileSystemEntry : public QObject {
Q_OBJECT Q_OBJECT
QML_ELEMENT QML_ELEMENT
QML_UNCREATABLE("FileSystemEntry instances can only be retrieved from a FileSystemModel") QML_UNCREATABLE("FileSystemEntry instances can only be retrieved from a FileSystemModel")
Q_PROPERTY(QString path READ path CONSTANT) Q_PROPERTY(QString path READ path CONSTANT)
Q_PROPERTY(QString relativePath READ relativePath NOTIFY relativePathChanged) Q_PROPERTY(QString relativePath READ relativePath NOTIFY relativePathChanged)
Q_PROPERTY(QString name READ name CONSTANT) Q_PROPERTY(QString name READ name CONSTANT)
Q_PROPERTY(QString baseName READ baseName CONSTANT) Q_PROPERTY(QString baseName READ baseName CONSTANT)
Q_PROPERTY(QString parentDir READ parentDir CONSTANT) Q_PROPERTY(QString parentDir READ parentDir CONSTANT)
Q_PROPERTY(QString suffix READ suffix CONSTANT) Q_PROPERTY(QString suffix READ suffix CONSTANT)
Q_PROPERTY(qint64 size READ size CONSTANT) Q_PROPERTY(qint64 size READ size CONSTANT)
Q_PROPERTY(bool isDir READ isDir CONSTANT) Q_PROPERTY(bool isDir READ isDir CONSTANT)
Q_PROPERTY(bool isImage READ isImage CONSTANT) Q_PROPERTY(bool isImage READ isImage CONSTANT)
Q_PROPERTY(QString mimeType READ mimeType CONSTANT) Q_PROPERTY(QString mimeType READ mimeType CONSTANT)
public: public:
explicit FileSystemEntry(const QString& path, const QString& relativePath, QObject* parent = nullptr); explicit FileSystemEntry(const QString& path, const QString& relativePath, QObject* parent = nullptr);
[[nodiscard]] QString path() const; [[nodiscard]] QString path() const;
[[nodiscard]] QString relativePath() const; [[nodiscard]] QString relativePath() const;
[[nodiscard]] QString name() const; [[nodiscard]] QString name() const;
[[nodiscard]] QString baseName() const; [[nodiscard]] QString baseName() const;
[[nodiscard]] QString parentDir() const; [[nodiscard]] QString parentDir() const;
[[nodiscard]] QString suffix() const; [[nodiscard]] QString suffix() const;
[[nodiscard]] qint64 size() const; [[nodiscard]] qint64 size() const;
[[nodiscard]] bool isDir() const; [[nodiscard]] bool isDir() const;
[[nodiscard]] bool isImage() const; [[nodiscard]] bool isImage() const;
[[nodiscard]] QString mimeType() const; [[nodiscard]] QString mimeType() const;
void updateRelativePath(const QDir& dir); void updateRelativePath(const QDir& dir);
signals: signals:
void relativePathChanged(); void relativePathChanged();
private: private:
const QFileInfo m_fileInfo; const QFileInfo m_fileInfo;
const QString m_path; const QString m_path;
QString m_relativePath; QString m_relativePath;
mutable bool m_isImage; mutable bool m_isImage;
mutable bool m_isImageInitialised; mutable bool m_isImageInitialised;
mutable QString m_mimeType; mutable QString m_mimeType;
mutable bool m_mimeTypeInitialised; mutable bool m_mimeTypeInitialised;
}; };
class FileSystemModel : public QAbstractListModel { class FileSystemModel : public QAbstractListModel {
Q_OBJECT Q_OBJECT
QML_ELEMENT QML_ELEMENT
Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged) Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged)
Q_PROPERTY(bool recursive READ recursive WRITE setRecursive NOTIFY recursiveChanged) Q_PROPERTY(bool recursive READ recursive WRITE setRecursive NOTIFY recursiveChanged)
Q_PROPERTY(bool watchChanges READ watchChanges WRITE setWatchChanges NOTIFY watchChangesChanged) Q_PROPERTY(bool watchChanges READ watchChanges WRITE setWatchChanges NOTIFY watchChangesChanged)
Q_PROPERTY(bool showHidden READ showHidden WRITE setShowHidden NOTIFY showHiddenChanged) Q_PROPERTY(bool showHidden READ showHidden WRITE setShowHidden NOTIFY showHiddenChanged)
Q_PROPERTY(bool sortReverse READ sortReverse WRITE setSortReverse NOTIFY sortReverseChanged) Q_PROPERTY(bool sortReverse READ sortReverse WRITE setSortReverse NOTIFY sortReverseChanged)
Q_PROPERTY(Filter filter READ filter WRITE setFilter NOTIFY filterChanged) Q_PROPERTY(Filter filter READ filter WRITE setFilter NOTIFY filterChanged)
Q_PROPERTY(QStringList nameFilters READ nameFilters WRITE setNameFilters NOTIFY nameFiltersChanged) Q_PROPERTY(QStringList nameFilters READ nameFilters WRITE setNameFilters NOTIFY nameFiltersChanged)
Q_PROPERTY(QQmlListProperty<ZShell::models::FileSystemEntry> entries READ entries NOTIFY entriesChanged) Q_PROPERTY(QQmlListProperty<ZShell::models::FileSystemEntry> entries READ entries NOTIFY entriesChanged)
public: public:
enum Filter { enum Filter {
NoFilter, NoFilter,
Images, Images,
Files, Files,
Dirs Dirs
}; };
Q_ENUM(Filter) Q_ENUM(Filter)
explicit FileSystemModel(QObject* parent = nullptr); explicit FileSystemModel(QObject* parent = nullptr);
int rowCount(const QModelIndex& parent = QModelIndex()) const override; int rowCount(const QModelIndex& parent = QModelIndex()) const override;
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
QHash<int, QByteArray> roleNames() const override; QHash<int, QByteArray> roleNames() const override;
[[nodiscard]] QString path() const; [[nodiscard]] QString path() const;
void setPath(const QString& path); void setPath(const QString& path);
[[nodiscard]] bool recursive() const; [[nodiscard]] bool recursive() const;
void setRecursive(bool recursive); void setRecursive(bool recursive);
[[nodiscard]] bool watchChanges() const; [[nodiscard]] bool watchChanges() const;
void setWatchChanges(bool watchChanges); void setWatchChanges(bool watchChanges);
[[nodiscard]] bool showHidden() const; [[nodiscard]] bool showHidden() const;
void setShowHidden(bool showHidden); void setShowHidden(bool showHidden);
[[nodiscard]] bool sortReverse() const; [[nodiscard]] bool sortReverse() const;
void setSortReverse(bool sortReverse); void setSortReverse(bool sortReverse);
[[nodiscard]] Filter filter() const; [[nodiscard]] Filter filter() const;
void setFilter(Filter filter); void setFilter(Filter filter);
[[nodiscard]] QStringList nameFilters() const; [[nodiscard]] QStringList nameFilters() const;
void setNameFilters(const QStringList& nameFilters); void setNameFilters(const QStringList& nameFilters);
[[nodiscard]] QQmlListProperty<FileSystemEntry> entries(); [[nodiscard]] QQmlListProperty<FileSystemEntry> entries();
signals: signals:
void pathChanged(); void pathChanged();
void recursiveChanged(); void recursiveChanged();
void watchChangesChanged(); void watchChangesChanged();
void showHiddenChanged(); void showHiddenChanged();
void sortReverseChanged(); void sortReverseChanged();
void filterChanged(); void filterChanged();
void nameFiltersChanged(); void nameFiltersChanged();
void entriesChanged(); void entriesChanged();
private: private:
QDir m_dir; QDir m_dir;
QFileSystemWatcher m_watcher; QFileSystemWatcher m_watcher;
QList<FileSystemEntry*> m_entries; QList<FileSystemEntry*> m_entries;
QHash<QString, QFuture<QPair<QSet<QString>, QSet<QString>>>> m_futures; QHash<QString, QFuture<QPair<QSet<QString>, QSet<QString> > > > m_futures;
QString m_path; QString m_path;
bool m_recursive; bool m_recursive;
bool m_watchChanges; bool m_watchChanges;
bool m_showHidden; bool m_showHidden;
bool m_sortReverse; bool m_sortReverse = false;
Filter m_filter; Filter m_filter;
QStringList m_nameFilters; QStringList m_nameFilters;
void watchDirIfRecursive(const QString& path); void watchDirIfRecursive(const QString& path);
void update(); void update();
void updateWatcher(); void updateWatcher();
void updateEntries(); void updateEntries();
void updateEntriesForDir(const QString& dir); void updateEntriesForDir(const QString& dir);
void applyChanges(const QSet<QString>& removedPaths, const QSet<QString>& addedPaths); void applyChanges(const QSet<QString>& removedPaths, const QSet<QString>& addedPaths);
[[nodiscard]] bool compareEntries(const FileSystemEntry* a, const FileSystemEntry* b) const; [[nodiscard]] bool compareEntries(const FileSystemEntry* a, const FileSystemEntry* b) const;
}; };
} // namespace ZShell::models } // namespace ZShell::models
+83 -307
View File
@@ -1,355 +1,131 @@
#include "writefile.hpp" #include "writefile.hpp"
#include <QtConcurrent/qtconcurrentrun.h> #include <QtConcurrent/qtconcurrentrun.h>
#include <QtCore/QCryptographicHash>
#include <QtCore/QSaveFile>
#include <QtGui/QImageReader>
#include <QtQuick/qquickimageprovider.h>
#include <QtQuick/qquickitemgrabresult.h> #include <QtQuick/qquickitemgrabresult.h>
#include <QtQuick/qquickwindow.h> #include <QtQuick/qquickwindow.h>
#include <qdir.h>
#include <QDir> #include <qfileinfo.h>
#include <QFile> #include <qfuturewatcher.h>
#include <QFileInfo> #include <qqmlengine.h>
#include <QFutureWatcher>
#include <QImage>
#include <QJSValue>
#include <QQmlEngine>
#include <QSize>
#include <QVariant>
namespace ZShell { namespace ZShell {
// ============================================================
// saveItem
// ============================================================
void ZShellIo::saveItem(QQuickItem* target, const QUrl& path) { void ZShellIo::saveItem(QQuickItem* target, const QUrl& path) {
this->saveItem(target, path, QRect(), QJSValue(), QJSValue()); this->saveItem(target, path, QRect(), QJSValue(), QJSValue());
} }
void ZShellIo::saveItem(QQuickItem* target, const QUrl& path, const QRect& rect) { void ZShellIo::saveItem(QQuickItem* target, const QUrl& path, const QRect& rect) {
this->saveItem(target, path, rect, QJSValue(), QJSValue()); this->saveItem(target, path, rect, QJSValue(), QJSValue());
} }
void ZShellIo::saveItem(QQuickItem* target, const QUrl& path, QJSValue onSaved) { void ZShellIo::saveItem(QQuickItem* target, const QUrl& path, QJSValue onSaved) {
this->saveItem(target, path, QRect(), onSaved, QJSValue()); this->saveItem(target, path, QRect(), onSaved, QJSValue());
} }
void ZShellIo::saveItem(QQuickItem* target, const QUrl& path, QJSValue onSaved, QJSValue onFailed) { void ZShellIo::saveItem(QQuickItem* target, const QUrl& path, QJSValue onSaved, QJSValue onFailed) {
this->saveItem(target, path, QRect(), onSaved, onFailed); this->saveItem(target, path, QRect(), onSaved, onFailed);
} }
void ZShellIo::saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved) { void ZShellIo::saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved) {
this->saveItem(target, path, rect, onSaved, QJSValue()); this->saveItem(target, path, rect, onSaved, QJSValue());
} }
void ZShellIo::saveItem( void ZShellIo::saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved, QJSValue onFailed) {
QQuickItem* target, if (!target) {
const QUrl& path, qWarning() << "ZShellIo::saveItem: a target is required";
const QRect& rect, return;
QJSValue onSaved, }
QJSValue onFailed
) {
if (!target) {
qWarning() << "ZShellIo::saveItem: a target is required";
return;
}
if (!path.isLocalFile()) { if (!path.isLocalFile()) {
qWarning() << "ZShellIo::saveItem:" << path << "is not a local file"; qWarning() << "ZShellIo::saveItem:" << path << "is not a local file";
return; return;
} }
if (!target->window()) { if (!target->window()) {
qWarning() << "ZShellIo::saveItem: unable to save target" qWarning() << "ZShellIo::saveItem: unable to save target" << target << "without a window";
<< target return;
<< "without a window"; }
return;
}
auto scaledRect = rect; auto scaledRect = rect;
const qreal scale = target->window()->devicePixelRatio();
if (rect.isValid() && !qFuzzyCompare(scale + 1.0, 2.0)) {
scaledRect =
QRectF(rect.left() * scale, rect.top() * scale, rect.width() * scale, rect.height() * scale).toRect();
}
const qreal scale = target->window()->devicePixelRatio(); const QSharedPointer<const QQuickItemGrabResult> grabResult = target->grabToImage();
if (rect.isValid() && !qFuzzyCompare(scale + 1.0, 2.0)) { QObject::connect(grabResult.data(), &QQuickItemGrabResult::ready, this,
scaledRect = QRectF( [grabResult, scaledRect, path, onSaved, onFailed, this]() {
rect.left() * scale, const auto future = QtConcurrent::run([=]() {
rect.top() * scale, QImage image = grabResult->image();
rect.width() * scale,
rect.height() * scale
).toRect();
}
const QSharedPointer<const QQuickItemGrabResult> grabResult = if (scaledRect.isValid()) {
target->grabToImage(); image = image.copy(scaledRect);
}
QObject::connect( const QString file = path.toLocalFile();
grabResult.data(), const QString parent = QFileInfo(file).absolutePath();
&QQuickItemGrabResult::ready, return QDir().mkpath(parent) && image.save(file);
this, });
[grabResult, scaledRect, path, onSaved, onFailed, this]() {
const auto future = QtConcurrent::run([grabResult, scaledRect, path]() {
QImage image = grabResult->image();
if (scaledRect.isValid()) { auto* watcher = new QFutureWatcher<bool>(this);
image = image.copy(scaledRect); auto* engine = qmlEngine(this);
}
const QString file = path.toLocalFile(); QObject::connect(watcher, &QFutureWatcher<bool>::finished, this, [=]() {
const QString parent = QFileInfo(file).absolutePath(); if (watcher->result()) {
if (onSaved.isCallable()) {
QDir().mkpath(parent); onSaved.call(
{ QJSValue(path.toLocalFile()), engine->toScriptValue(QVariant::fromValue(path)) });
QSaveFile out(file); }
if (!out.open(QIODevice::WriteOnly)) { } else {
return false; qWarning() << "ZShellIo::saveItem: failed to save" << path;
} if (onFailed.isCallable()) {
onFailed.call({ engine->toScriptValue(QVariant::fromValue(path)) });
if (!image.save(&out, "PNG")) { }
return false; }
} watcher->deleteLater();
});
return out.commit(); watcher->setFuture(future);
}); });
auto* watcher = new QFutureWatcher<bool>(this);
auto* engine = qmlEngine(this);
QObject::connect(watcher, &QFutureWatcher<bool>::finished, this, [=]() {
if (watcher->result()) {
if (onSaved.isCallable() && engine) {
onSaved.call({
engine->toScriptValue(path.toLocalFile()),
engine->toScriptValue(path)
});
}
} else {
qWarning() << "ZShellIo::saveItem: failed to save" << path;
if (onFailed.isCallable() && engine) {
onFailed.call({
engine->toScriptValue(path)
});
}
}
watcher->deleteLater();
});
watcher->setFuture(future);
}
);
} }
// ============================================================
// cacheImage
// ============================================================
void ZShellIo::cacheImage(const QUrl& source, const QString& cacheDir) {
this->cacheImage(source, cacheDir, QJSValue(), QJSValue());
}
void ZShellIo::cacheImage(const QUrl& source, const QString& cacheDir, QJSValue onSaved) {
this->cacheImage(source, cacheDir, onSaved, QJSValue());
}
void ZShellIo::cacheImage(
const QUrl& source,
const QString& cacheDir,
QJSValue onSaved,
QJSValue onFailed
) {
if (cacheDir.isEmpty()) {
qWarning() << "ZShellIo::cacheImage: cacheDir is empty";
return;
}
QImage image;
if (!loadSourceImage(source, image)) {
qWarning() << "ZShellIo::cacheImage: failed to load source image" << source;
auto* engine = qmlEngine(this);
if (onFailed.isCallable() && engine) {
onFailed.call({
engine->toScriptValue(source),
engine->toScriptValue(cacheDir)
});
}
return;
}
const auto future = QtConcurrent::run([image, cacheDir]() -> QString {
if (image.isNull()) {
return QString();
}
const QImage normalized = image.convertToFormat(QImage::Format_RGBA8888);
const QByteArray bytes(
reinterpret_cast<const char*>(normalized.constBits()),
qsizetype(normalized.sizeInBytes())
);
const QByteArray digest =
QCryptographicHash::hash(bytes, QCryptographicHash::Sha256).toHex();
QDir dir(cacheDir);
if (!dir.exists() && !QDir().mkpath(cacheDir)) {
return QString();
}
const QString finalPath = dir.filePath(QString::fromLatin1(digest) + ".png");
if (QFile::exists(finalPath)) {
return finalPath;
}
QSaveFile out(finalPath);
if (!out.open(QIODevice::WriteOnly)) {
return QString();
}
if (!normalized.save(&out, "PNG")) {
return QString();
}
if (!out.commit()) {
return QString();
}
return finalPath;
});
auto* watcher = new QFutureWatcher<QString>(this);
auto* engine = qmlEngine(this);
QObject::connect(watcher, &QFutureWatcher<QString>::finished, this, [=]() {
const QString finalPath = watcher->result();
if (!finalPath.isEmpty()) {
if (onSaved.isCallable() && engine) {
onSaved.call({
engine->toScriptValue(finalPath),
engine->toScriptValue(QUrl::fromLocalFile(finalPath))
});
}
} else {
qWarning() << "ZShellIo::cacheImage: failed to cache" << source;
if (onFailed.isCallable() && engine) {
onFailed.call({
engine->toScriptValue(source),
engine->toScriptValue(cacheDir)
});
}
}
watcher->deleteLater();
});
watcher->setFuture(future);
}
// ============================================================
// loadSourceImage
// ============================================================
bool ZShellIo::loadSourceImage(const QUrl& source, QImage& image) const {
image = QImage();
if (source.isLocalFile()) {
QImageReader reader(source.toLocalFile());
reader.setAutoTransform(true);
image = reader.read();
return !image.isNull();
}
if (source.scheme() == "image") {
auto* engine = qmlEngine(const_cast<ZShellIo*>(this));
if (!engine) {
qWarning() << "ZShellIo::loadSourceImage: no QQmlEngine";
return false;
}
const QString providerId = source.host();
const QString imageId =
source.path().startsWith('/')
? source.path().mid(1)
: source.path();
auto* providerBase = engine->imageProvider(providerId);
if (!providerBase) {
qWarning() << "ZShellIo::loadSourceImage: provider not found"
<< providerId;
return false;
}
auto* provider = dynamic_cast<QQuickImageProvider*>(providerBase);
if (!provider) {
qWarning() << "ZShellIo::loadSourceImage: provider is not a QQuickImageProvider"
<< providerId;
return false;
}
QSize size;
switch (provider->imageType()) {
case QQuickImageProvider::Image:
image = provider->requestImage(imageId, &size, QSize());
break;
case QQuickImageProvider::Pixmap:
image = provider->requestPixmap(imageId, &size, QSize()).toImage();
break;
default:
qWarning() << "ZShellIo::loadSourceImage: unsupported provider type"
<< providerId;
return false;
}
return !image.isNull();
}
qWarning() << "ZShellIo::loadSourceImage: unsupported source" << source;
return false;
}
// ============================================================
// File ops
// ============================================================
bool ZShellIo::copyFile(const QUrl& source, const QUrl& target, bool overwrite) const { bool ZShellIo::copyFile(const QUrl& source, const QUrl& target, bool overwrite) const {
if (!source.isLocalFile()) { if (!source.isLocalFile()) {
qWarning() << "ZShellIo::copyFile: source" << source << "is not a local file"; qWarning() << "ZShellIo::copyFile: source" << source << "is not a local file";
return false; return false;
} }
if (!target.isLocalFile()) { if (!target.isLocalFile()) {
qWarning() << "ZShellIo::copyFile: target" << target << "is not a local file"; qWarning() << "ZShellIo::copyFile: target" << target << "is not a local file";
return false; return false;
} }
if (overwrite) { if (overwrite) {
QFile::remove(target.toLocalFile()); if (!QFile::remove(target.toLocalFile())) {
} qWarning() << "ZShellIo::copyFile: overwrite was specified but failed to remove" << target.toLocalFile();
return false;
}
}
return QFile::copy(source.toLocalFile(), target.toLocalFile()); return QFile::copy(source.toLocalFile(), target.toLocalFile());
} }
bool ZShellIo::deleteFile(const QUrl& path) const { bool ZShellIo::deleteFile(const QUrl& path) const {
if (!path.isLocalFile()) { if (!path.isLocalFile()) {
qWarning() << "ZShellIo::deleteFile: path" << path << "is not a local file"; qWarning() << "ZShellIo::deleteFile: path" << path << "is not a local file";
return false; return false;
} }
return QFile::remove(path.toLocalFile()); return QFile::remove(path.toLocalFile());
} }
QString ZShellIo::toLocalFile(const QUrl& url) const { QString ZShellIo::toLocalFile(const QUrl& url) const {
if (!url.isLocalFile()) { if (!url.isLocalFile()) {
qWarning() << "ZShellIo::toLocalFile: given url is not a local file" << url; qWarning() << "ZShellIo::toLocalFile: given url is not a local file" << url;
return QString(); return QString();
} }
return url.toLocalFile(); return url.toLocalFile();
} }
} // namespace ZShell } // namespace ZShell
+16 -25
View File
@@ -1,40 +1,31 @@
#pragma once #pragma once
#include <QtQuick/qquickitem.h> #include <QtQuick/qquickitem.h>
#include <QImage> #include <qobject.h>
#include <QJSValue>
#include <QObject>
#include <qqmlintegration.h> #include <qqmlintegration.h>
#include <QUrl>
namespace ZShell { namespace ZShell {
class ZShellIo : public QObject { class ZShellIo : public QObject {
Q_OBJECT Q_OBJECT
QML_ELEMENT QML_ELEMENT
QML_SINGLETON QML_SINGLETON
public: public:
// clang-format off // clang-format off
Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path); Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path);
Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, const QRect& rect); Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, const QRect& rect);
Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, QJSValue onSaved); Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, QJSValue onSaved);
Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, QJSValue onSaved, QJSValue onFailed); Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, QJSValue onSaved, QJSValue onFailed);
Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved); Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved);
Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved, QJSValue onFailed); Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved, QJSValue onFailed);
// clang-format on
Q_INVOKABLE void cacheImage(const QUrl& source, const QString& cacheDir); Q_INVOKABLE bool copyFile(const QUrl& source, const QUrl& target, bool overwrite = true) const;
Q_INVOKABLE void cacheImage(const QUrl& source, const QString& cacheDir, QJSValue onSaved); Q_INVOKABLE bool deleteFile(const QUrl& path) const;
Q_INVOKABLE void cacheImage(const QUrl& source, const QString& cacheDir, QJSValue onSaved, QJSValue onFailed); Q_INVOKABLE QString toLocalFile(const QUrl& url) const;
// clang-format on
Q_INVOKABLE bool copyFile(const QUrl& source, const QUrl& target, bool overwrite = true) const;
Q_INVOKABLE bool deleteFile(const QUrl& path) const;
Q_INVOKABLE QString toLocalFile(const QUrl& url) const;
private:
bool loadSourceImage(const QUrl& source, QImage& image) const;
}; };
} // namespace ZShell } // namespace ZShell
+2 -38
View File
@@ -1,52 +1,16 @@
from __future__ import annotations from __future__ import annotations
import sys
from pathlib import Path
import typer import typer
from typer._completion_shared import install, _get_shell_name
from zshell.subcommands import shell, scheme, screenshot, wallpaper, record from zshell.subcommands import shell, scheme, screenshot, wallpaper, record
app = typer.Typer(name="zshell-cli", add_completion=False) app = typer.Typer()
app.add_typer(shell.app, name="shell") app.add_typer(shell.app, name="shell")
app.add_typer(scheme.app, name="scheme") app.add_typer(scheme.app, name="scheme")
app.add_typer(screenshot.app, name="screenshot") app.add_typer(screenshot.app, name="screenshot")
app.add_typer(wallpaper.app, name="wallpaper") app.add_typer(wallpaper.app, name="wallpaper")
app.add_typer(record.app, name="record") app.add_typer(record.app, name="record")
# app.add_typer(preset.app, name="preset")
def _completion_installed() -> bool:
shell = _get_shell_name()
match shell:
case "zsh":
return (Path.home() / ".zfunc" / "_zshell-cli").exists()
case "bash":
return (Path.home() / ".bash_completions" / "zshell-cli.sh").exists()
case "fish":
return (Path.home() / ".config" / "fish" / "completions" / "zshell-cli.fish").exists()
return False
def _install_completion() -> None:
if _completion_installed():
print("zshell-cli: Shell completion already installed.")
raise typer.Exit()
shell = _get_shell_name()
if shell is None:
print("zshell-cli: Unable to detect shell type.", file=sys.stderr)
raise typer.Exit(code=1)
try:
_, path = install(prog_name="zshell-cli")
print(f"zshell-cli: Shell completion installed ({shell}: {path})")
print("zshell-cli: Restart your shell or source the file to enable tab-completion.")
except Exception:
pass
def main() -> None: def main() -> None:
if "--install-autocomplete" in sys.argv:
_install_completion()
return
if sys.stdout.isatty() and not _completion_installed():
print("zshell-cli: Tip: run with --install-autocomplete for tab completion.", file=sys.stderr)
app() app()
+24 -20
View File
@@ -18,7 +18,8 @@ TEMP_RECORDING = STATE_DIR / "recording.mp4"
REPLAY_RECORDING = STATE_DIR / "replay.mp4" REPLAY_RECORDING = STATE_DIR / "replay.mp4"
NOTIF_ID_FILE = STATE_DIR / "notifid.txt" NOTIF_ID_FILE = STATE_DIR / "notifid.txt"
RECORDINGS_DIR = os.getenv("ZSHELL_RECORDINGS_DIR", str(Path(HOME) / "Videos/Recordings")) RECORDINGS_DIR = os.getenv("ZSHELL_RECORDINGS_DIR",
str(Path(HOME) / "Videos/Recordings"))
def _read_extra_args() -> list[str]: def _read_extra_args() -> list[str]:
@@ -35,7 +36,7 @@ def _is_recording() -> bool:
return subprocess.run(["pidof", RECORDER], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode == 0 return subprocess.run(["pidof", RECORDER], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode == 0
def _notify(summary: str, body: str = "", actions: list | None = None, timeout: int = 5000) -> Optional[int]: def _notify(summary: str, body: str = "", actions: list = None, timeout: int = 5000) -> Optional[int]:
args = ["notify-send", summary, body, "-t", str(timeout), "-p"] args = ["notify-send", summary, body, "-t", str(timeout), "-p"]
if actions: if actions:
for action in actions: for action in actions:
@@ -48,12 +49,14 @@ def _notify(summary: str, body: str = "", actions: list | None = None, timeout:
def _close_notification(notif_id: int): def _close_notification(notif_id: int):
subprocess.run(["notify-send", "--close", str(notif_id)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) subprocess.run(["notify-send", "--close", str(notif_id)],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
def _get_monitors() -> list[dict]: def _get_monitors() -> list[dict]:
try: try:
res = subprocess.run(["hyprctl", "monitors", "-j"], capture_output=True, text=True) res = subprocess.run(["hyprctl", "monitors", "-j"],
capture_output=True, text=True)
return json.loads(res.stdout) return json.loads(res.stdout)
except Exception: except Exception:
return [] return []
@@ -89,7 +92,6 @@ def _slurp_region() -> Optional[str]:
def _parse_geometry(geometry: str) -> Optional[tuple[int, int, int, int]]: def _parse_geometry(geometry: str) -> Optional[tuple[int, int, int, int]]:
import re import re
match = re.match(r"(\d+)x(\d+)\+(\d+)\+(\d+)", geometry) match = re.match(r"(\d+)x(\d+)\+(\d+)\+(\d+)", geometry)
if match: if match:
return int(match.group(3)), int(match.group(4)), int(match.group(1)), int(match.group(2)) return int(match.group(3)), int(match.group(4)), int(match.group(1)), int(match.group(2))
@@ -137,7 +139,8 @@ def start_recording(region: Optional[str], sound: bool):
cmd.extend(extra_args) cmd.extend(extra_args)
cmd.extend(["-o", str(TEMP_RECORDING)]) cmd.extend(["-o", str(TEMP_RECORDING)])
subprocess.Popen(cmd, start_new_session=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) subprocess.Popen(cmd, start_new_session=True,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
notif_id = _notify("Recording started", f"Saving to {TEMP_RECORDING}") notif_id = _notify("Recording started", f"Saving to {TEMP_RECORDING}")
if notif_id is not None: if notif_id is not None:
@@ -145,12 +148,14 @@ def start_recording(region: Optional[str], sound: bool):
time.sleep(1) time.sleep(1)
if not _is_recording(): if not _is_recording():
_notify("Recording failed", "Check gpu-screen-recorder output.", timeout=5000) _notify("Recording failed",
"Check gpu-screen-recorder output.", timeout=5000)
raise typer.Exit(code=1) raise typer.Exit(code=1)
def stop_recording(clipboard: bool): def stop_recording(clipboard: bool):
subprocess.run(["pkill", "-f", RECORDER], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) subprocess.run(["pkill", "-f", RECORDER],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
for _ in range(50): for _ in range(50):
if not _is_recording(): if not _is_recording():
@@ -173,31 +178,30 @@ def stop_recording(clipboard: bool):
NOTIF_ID_FILE.unlink() NOTIF_ID_FILE.unlink()
if clipboard: if clipboard:
subprocess.run( subprocess.run(["wl-copy", "--type", "text/uri-list", f"file://{final_path}"],
["wl-copy", "--type", "text/uri-list", f"file://{final_path}"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
_notify("Recording stopped", f"Saved to {final_path}", timeout=5000) _notify("Recording stopped", f"Saved to {final_path}", timeout=5000)
def toggle_pause(): def toggle_pause():
subprocess.run(["pkill", "-USR2", "-f", RECORDER], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) subprocess.run(["pkill", "-USR2", "-f", RECORDER],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
typer.echo("Toggled pause.") typer.echo("Toggled pause.")
@app.command() @app.command()
def record( def record(
region: Optional[str] = typer.Option( region: Optional[str] = typer.Option(
None, None, "--region", "-r",
"--region",
"-r",
help="Record a region. Use 'slurp' (or omit value) to select interactively, or give 'WxH+X+Y'.", help="Record a region. Use 'slurp' (or omit value) to select interactively, or give 'WxH+X+Y'.",
), ),
sound: bool = typer.Option(False, "--sound", "-s", help="Record audio from default output."), sound: bool = typer.Option(
pause: bool = typer.Option(False, "--pause", "-p", help="Toggle pause/resume."), False, "--sound", "-s", help="Record audio from default output."),
clipboard: bool = typer.Option(False, "--clipboard", "-c", help="Copy the final recording path to clipboard."), pause: bool = typer.Option(
False, "--pause", "-p", help="Toggle pause/resume."),
clipboard: bool = typer.Option(
False, "--clipboard", "-c", help="Copy the final recording path to clipboard."),
): ):
"""Start or stop a screen recording with gpu-screen-recorder.""" """Start or stop a screen recording with gpu-screen-recorder."""
if pause: if pause:
+15 -107
View File
@@ -2,7 +2,6 @@ import typer
import json import json
import shutil import shutil
import os import os
import sys
import re import re
import subprocess import subprocess
@@ -16,61 +15,11 @@ from materialyoucolor.score.score import Score
from materialyoucolor.dynamiccolor.material_dynamic_colors import MaterialDynamicColors from materialyoucolor.dynamiccolor.material_dynamic_colors import MaterialDynamicColors
from materialyoucolor.hct.hct import Hct from materialyoucolor.hct.hct import Hct
from materialyoucolor.utils.color_utils import argb_from_rgb from materialyoucolor.utils.color_utils import argb_from_rgb
from materialyoucolor.utils.math_utils import ( from materialyoucolor.utils.math_utils import difference_degrees, rotation_direction, sanitize_degrees_double
difference_degrees,
rotation_direction,
sanitize_degrees_double,
)
app = typer.Typer() app = typer.Typer()
def _complete_scheme_name(incomplete):
schemes = [
"fruit-salad",
"expressive",
"monochrome",
"rainbow",
"tonal-spot",
"neutral",
"fidelity",
"content",
"vibrant",
]
return [s for s in schemes if incomplete in s]
def _complete_preset(incomplete):
results = []
for sid, meta in list_schemes().items():
for v in meta.variants:
preset = f"{sid}:{v.id}"
if incomplete in preset:
results.append((preset, f"{meta.name} - {v.name}"))
return results
def _complete_mode(incomplete):
return [m for m in ("dark", "light") if incomplete in m]
def _complete_accent(ctx, incomplete):
preset_val = ctx.params.get("preset")
if preset_val:
try:
p_scheme, p_variant = resolve_preset(preset_val)
for v in list_schemes()[p_scheme].variants:
if v.id == p_variant:
return [a for a in v.accents if incomplete in a]
except (ValueError, KeyError):
pass
all_accents = set()
for meta in list_schemes().values():
for v in meta.variants:
all_accents.update(v.accents)
return [a for a in sorted(all_accents) if incomplete in a]
@app.command() @app.command()
def list_presets( def list_presets(
json_format: bool = typer.Option(False, "--json", help="Output in JSON format"), json_format: bool = typer.Option(False, "--json", help="Output in JSON format"),
@@ -81,7 +30,7 @@ def list_presets(
for sid, meta in sorted(schemes.items()): for sid, meta in sorted(schemes.items()):
variants = {} variants = {}
for v in meta.variants: for v in meta.variants:
entry: dict[str, Any] = {"modes": sorted(v.modes)} entry = {"modes": sorted(v.modes)}
if v.accents: if v.accents:
entry["accents"] = sorted(v.accents) entry["accents"] = sorted(v.accents)
entry["default_accent"] = sorted(v.accents)[0] entry["default_accent"] = sorted(v.accents)[0]
@@ -106,35 +55,14 @@ def list_presets(
@app.command() @app.command()
def generate( def generate(
image_path: Optional[Path] = typer.Option( image_path: Optional[Path] = typer.Option(None, help="Path to source image. Required for image mode."),
None, help="Path to source image. Required for image mode."
),
scheme: Optional[str] = typer.Option( scheme: Optional[str] = typer.Option(
None, None, help="Color scheme algorithm to use for image mode. Ignored in preset mode."
help="Color scheme algorithm to use for image mode. Ignored in preset mode.",
autocompletion=_complete_scheme_name,
),
preset: Optional[str] = typer.Option(
None,
help="Name of a premade scheme in this format: <scheme>:<variant>",
autocompletion=_complete_preset,
),
mode: Optional[str] = typer.Option(
None,
help="Mode of the preset scheme (dark or light).",
autocompletion=_complete_mode,
),
accent: Optional[str] = typer.Option(
None,
help="Accent for schemes that support it (e.g. mauve).",
autocompletion=_complete_accent,
), ),
preset: Optional[str] = typer.Option(None, help="Name of a premade scheme in this format: <scheme>:<variant>"),
mode: Optional[str] = typer.Option(None, help="Mode of the preset scheme (dark or light)."),
accent: Optional[str] = typer.Option(None, help="Accent for schemes that support it (e.g. mauve)."),
): ):
if not any([image_path, scheme, preset, mode, accent]):
print(
"Hint: use --preset <scheme>:<variant> or --image-path <path>",
file=sys.stderr,
)
HOME = str(os.getenv("HOME")) HOME = str(os.getenv("HOME"))
OUTPUT = Path(HOME + "/.local/state/zshell/scheme.json") OUTPUT = Path(HOME + "/.local/state/zshell/scheme.json")
@@ -272,15 +200,11 @@ def generate(
def harmonize(from_hct: Hct, to_hct: Hct, tone_boost: float) -> Hct: def harmonize(from_hct: Hct, to_hct: Hct, tone_boost: float) -> Hct:
diff = difference_degrees(from_hct.hue, to_hct.hue) diff = difference_degrees(from_hct.hue, to_hct.hue)
rotation = min(diff * 0.8, 100) rotation = min(diff * 0.8, 100)
output_hue = sanitize_degrees_double( output_hue = sanitize_degrees_double(from_hct.hue + rotation * rotation_direction(from_hct.hue, to_hct.hue))
from_hct.hue + rotation * rotation_direction(from_hct.hue, to_hct.hue)
)
tone = max(0.0, min(100.0, from_hct.tone * (1 + tone_boost))) tone = max(0.0, min(100.0, from_hct.tone * (1 + tone_boost)))
return Hct.from_hct(output_hue, from_hct.chroma, tone) return Hct.from_hct(output_hue, from_hct.chroma, tone)
def terminal_palette( def terminal_palette(colors: dict[str, str], mode: str, variant: str) -> dict[str, str]:
colors: dict[str, str], mode: str, variant: str
) -> dict[str, str]:
light = mode.lower() == "light" light = mode.lower() == "light"
key_hex = ( key_hex = (
@@ -312,7 +236,7 @@ def generate(
image = Image.open(image_path) image = Image.open(image_path)
image = image.convert("RGB") image = image.convert("RGB")
image.thumbnail(size, Image.Resampling.NEAREST) image.thumbnail(size, Image.NEAREST)
thumbnail_file.parent.mkdir(parents=True, exist_ok=True) thumbnail_file.parent.mkdir(parents=True, exist_ok=True)
image.save(thumbnail_path, "JPEG") image.save(thumbnail_path, "JPEG")
@@ -344,15 +268,8 @@ def generate(
is_dark = "" is_dark = ""
with Image.open(image_path) as img: with Image.open(image_path) as img:
img.thumbnail((1, 1), Image.Resampling.LANCZOS) img.thumbnail((1, 1), Image.LANCZOS)
px = img.getpixel((0, 0)) hct = Hct.from_int(argb_from_rgb(*img.getpixel((0, 0))))
if isinstance(px, (int, float)):
r = g = b = int(px)
elif px is not None:
r, g, b = int(px[0]), int(px[1]), int(px[2])
else:
r = g = b = 0
hct = Hct.from_int(argb_from_rgb(r, g, b))
is_dark = "light" if hct.tone > 50 else "dark" is_dark = "light" if hct.tone > 50 else "dark"
return is_dark return is_dark
@@ -514,8 +431,6 @@ def generate(
raw = tpl_path.read_text(encoding="utf-8") raw = tpl_path.read_text(encoding="utf-8")
out_path, body = split_directive_and_body(raw) out_path, body = split_directive_and_body(raw)
if out_path is None:
continue
out_path.parent.mkdir(parents=True, exist_ok=True) out_path.parent.mkdir(parents=True, exist_ok=True)
@@ -569,30 +484,23 @@ def generate(
with CONFIG.open() as f: with CONFIG.open() as f:
config = json.load(f) config = json.load(f)
scheme_type = config["colors"].get("schemeType", "fruit-salad") scheme = scheme or config["colors"]["schemeType"]
scheme = scheme or scheme_type
assert isinstance(scheme, str)
config_mode = config["general"]["color"]["mode"] config_mode = config["general"]["color"]["mode"]
smart = bool(config["general"]["color"].get("smart", False)) smart = bool(config["general"]["color"].get("smart", False))
scheme_class = get_scheme_class(scheme) scheme_class = get_scheme_class(scheme)
p_variant = "default"
if preset: if preset:
p_scheme, p_variant = resolve_preset(preset) p_scheme, p_variant = resolve_preset(preset)
schemes = list_schemes() schemes = list_schemes()
if accent and p_scheme in schemes: if accent and p_scheme in schemes:
meta = schemes[p_scheme] meta = schemes[p_scheme]
var_accents = next( var_accents = next((v.accents for v in meta.variants if v.id == p_variant), ())
(v.accents for v in meta.variants if v.id == p_variant), ()
)
if accent not in var_accents: if accent not in var_accents:
available = ", ".join(var_accents) if var_accents else "none" available = ", ".join(var_accents) if var_accents else "none"
raise typer.BadParameter( raise typer.BadParameter(
f"Accent '{accent}' not available for '{p_scheme}:{p_variant}'. Available accents: {available}" f"Accent '{accent}' not available for '{p_scheme}:{p_variant}'. Available accents: {available}"
) )
palette_obj = get_palette( palette_obj = get_palette(p_scheme, p_variant, mode or config_mode, accent=accent)
p_scheme, p_variant, mode or config_mode, accent=accent
)
colors = palette_obj.colors colors = palette_obj.colors
effective_mode = palette_obj.mode effective_mode = palette_obj.mode
name = palette_obj.scheme name = palette_obj.scheme
+8 -15
View File
@@ -2,6 +2,7 @@ import subprocess
import sys import sys
import time import time
import click
import typer import typer
args = ["qs", "-c", "zshell"] args = ["qs", "-c", "zshell"]
@@ -13,8 +14,7 @@ app = typer.Typer()
def kill(): def kill():
result = subprocess.run(args + ["kill"], capture_output=True) result = subprocess.run(args + ["kill"], capture_output=True)
if result.returncode != 0: if result.returncode != 0:
sys.stderr.write("No running instance to kill.\n") raise click.ClickException("No running instance to kill.")
sys.exit(1)
sys.stderr.write(result.stderr.decode()) sys.stderr.write(result.stderr.decode())
@@ -23,12 +23,10 @@ def start_instance(no_daemon: bool = False) -> None:
stdout = result.stdout.decode().strip() stdout = result.stdout.decode().strip()
if stdout: if stdout:
if "already running" in stdout.lower(): if "already running" in stdout.lower():
sys.stderr.write(stdout + "\n") raise click.ClickException(stdout)
sys.exit(1)
if result.returncode != 0: if result.returncode != 0:
stderr = result.stderr.decode().strip() stderr = result.stderr.decode().strip()
sys.stderr.write(stderr + "\n") raise click.ClickException(stderr)
sys.exit(1)
@app.command() @app.command()
@@ -52,9 +50,7 @@ def restart(no_daemon: bool = False):
def show(): def show():
result = subprocess.run(args + ["ipc"] + ["show"], capture_output=True) result = subprocess.run(args + ["ipc"] + ["show"], capture_output=True)
if result.returncode != 0: if result.returncode != 0:
sys.stderr.write(result.stderr.decode()) raise click.ClickException(result.stderr.decode().strip())
sys.exit(1)
sys.stdout.write(result.stdout.decode())
sys.stderr.write(result.stderr.decode()) sys.stderr.write(result.stderr.decode())
@@ -62,8 +58,7 @@ def show():
def log(): def log():
result = subprocess.run(args + ["log"], capture_output=True) result = subprocess.run(args + ["log"], capture_output=True)
if result.returncode != 0: if result.returncode != 0:
sys.stderr.write(result.stderr.decode()) raise click.ClickException(result.stderr.decode().strip())
sys.exit(1)
sys.stdout.write(result.stdout.decode()) sys.stdout.write(result.stdout.decode())
sys.stderr.write(result.stderr.decode()) sys.stderr.write(result.stderr.decode())
@@ -72,8 +67,7 @@ def log():
def lock(): def lock():
result = subprocess.run(args + ["ipc"] + ["call"] + ["lock"] + ["lock"], capture_output=True) result = subprocess.run(args + ["ipc"] + ["call"] + ["lock"] + ["lock"], capture_output=True)
if result.returncode != 0: if result.returncode != 0:
sys.stderr.write(result.stderr.decode()) raise click.ClickException(result.stderr.decode().strip())
sys.exit(1)
sys.stderr.write(result.stderr.decode()) sys.stderr.write(result.stderr.decode())
@@ -81,6 +75,5 @@ def lock():
def call(target: str, method: str, method_args: list[str] = typer.Argument(None)): def call(target: str, method: str, method_args: list[str] = typer.Argument(None)):
result = subprocess.run(args + ["ipc"] + ["call"] + [target] + [method] + (method_args or []), capture_output=True) result = subprocess.run(args + ["ipc"] + ["call"] + [target] + [method] + (method_args or []), capture_output=True)
if result.returncode != 0: if result.returncode != 0:
sys.stderr.write(result.stderr.decode()) raise click.ClickException(result.stderr.decode().strip())
sys.exit(1)
sys.stderr.write(result.stderr.decode()) sys.stderr.write(result.stderr.decode())
+2 -2
View File
@@ -34,9 +34,9 @@ def lockscreen(
return return
if size[0] < 3840 or size[1] < 2160: if size[0] < 3840 or size[1] < 2160:
img = img.resize((size[0] // 2, size[1] // 2), Image.Resampling.NEAREST) img = img.resize((size[0] // 2, size[1] // 2), Image.NEAREST)
else: else:
img = img.resize((size[0] // 4, size[1] // 4), Image.Resampling.NEAREST) img = img.resize((size[0] // 4, size[1] // 4), Image.NEAREST)
img = img.filter(ImageFilter.GaussianBlur(blur_amount)) img = img.filter(ImageFilter.GaussianBlur(blur_amount))
+2 -3
View File
@@ -62,9 +62,8 @@ class TestStart:
class TestShow: class TestShow:
@patch("zshell.subcommands.shell.subprocess.run") @patch("zshell.subcommands.shell.subprocess.run")
def test_show_runs_ipc_show(self, mock_run): def test_show_runs_ipc_show(self, mock_run):
mock_run.return_value = CompletedProcess([], 0, b"target visibilities\n", b"") mock_run.return_value = CompletedProcess([], 0, b"", b"target visibilities\n")
result = invoke("show") invoke("show")
assert "target visibilities" in result.output
mock_run.assert_called_once_with(["qs", "-c", "zshell", "ipc", "show"], capture_output=True) mock_run.assert_called_once_with(["qs", "-c", "zshell", "ipc", "show"], capture_output=True)
-8
View File
@@ -180,14 +180,6 @@ export const settingsIndex = [
section: "Bar", section: "Bar",
keywords: ["smoothing", "rounding"], keywords: ["smoothing", "rounding"],
}, },
// System tray section
{
name: "Tray icon size",
category: "bar",
categoryName: "Bar",
section: "Tray",
keywords: ["tray", "icon", "size"],
},
// Popouts section // Popouts section
{ {
name: "Tray", name: "Tray",
+3 -12
View File
@@ -1,25 +1,20 @@
//@ pragma UseQApplication //@ pragma UseQApplication
//@ pragma Env QSG_RENDER_LOOP=threaded //@ pragma Env QSG_RENDER_LOOP=threaded
// @ pragma Env QSG_RHI_BACKEND=vulkan //@ pragma Env QSG_RHI_BACKEND=vulkan
//@ pragma Env QSG_NO_VSYNC=1 //@ pragma Env QSG_NO_VSYNC=1
//@ pragma Env QS_NO_RELOAD_POPUP=1 //@ pragma Env QS_NO_RELOAD_POPUP=1
//@ pragma Env QT_SCALE_FACTOR_ROUNDING_POLICY=Round //@ pragma Env QT_SCALE_FACTOR_ROUNDING_POLICY=Round
//@ pragma DropExpensiveFonts //@ pragma DropExpensiveFonts
import Quickshell import Quickshell
import Quickshell.Services.UPower import qs.Extensions
import qs.Modules import qs.Modules
import qs.Modules.Wallpaper import qs.Modules.Wallpaper
import qs.Modules.Lock import qs.Modules.Lock
import qs.Drawers import qs.Drawers
import qs.Helpers import qs.Helpers
import qs.Modules.Polkit import qs.Modules.Polkit
import qs.Daemons
ShellRoot { ShellRoot {
id: root
readonly property bool laptop: UPower.displayDevice.isLaptopBattery
settings.watchFiles: true settings.watchFiles: true
Windows { Windows {
@@ -45,10 +40,6 @@ ShellRoot {
Polkit { Polkit {
} }
LazyLoader { LoadExtensions {
activeAsync: root.laptop
component: Battery {
}
} }
} }
+807 -3
View File
@@ -8,12 +8,47 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "aligned"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685"
dependencies = [
"as-slice",
]
[[package]]
name = "aligned-vec"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b"
dependencies = [
"equator",
]
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.102" version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
[[package]]
name = "arg_enum_proc_macro"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "arrayref" name = "arrayref"
version = "0.3.9" version = "0.3.9"
@@ -26,18 +61,103 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "as-slice"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516"
dependencies = [
"stable_deref_trait",
]
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.5.0" version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "av-scenechange"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394"
dependencies = [
"aligned",
"anyhow",
"arg_enum_proc_macro",
"arrayvec",
"log",
"num-rational",
"num-traits",
"pastey",
"rayon",
"thiserror",
"v_frame",
"y4m",
]
[[package]]
name = "av1-grain"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8"
dependencies = [
"anyhow",
"arrayvec",
"log",
"nom",
"num-rational",
"v_frame",
]
[[package]]
name = "avif-serialize"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "375082f007bd67184fb9c0374614b29f9aaa604ec301635f72338bb65386a53d"
dependencies = [
"arrayvec",
]
[[package]]
name = "bit_field"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.11.1" version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
[[package]]
name = "bitstream-io"
version = "4.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7eff00be299a18769011411c9def0d827e8f2d7bf0c3dbf53633147a8867fd1f"
dependencies = [
"no_std_io2",
]
[[package]]
name = "built"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64"
[[package]]
name = "bumpalo"
version = "3.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
[[package]] [[package]]
name = "bytemuck" name = "bytemuck"
version = "1.25.0" version = "1.25.0"
@@ -50,12 +170,30 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]]
name = "cc"
version = "1.2.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d"
dependencies = [
"find-msvc-tools",
"jobserver",
"libc",
"shlex",
]
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.4" version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "color_quant"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]] [[package]]
name = "crc32fast" name = "crc32fast"
version = "1.5.0" version = "1.5.0"
@@ -65,6 +203,84 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "crossbeam-deque"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crunchy"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "equator"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc"
dependencies = [
"equator-macro",
]
[[package]]
name = "equator-macro"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "exr"
version = "1.74.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be"
dependencies = [
"bit_field",
"half",
"lebe",
"miniz_oxide",
"rayon-core",
"smallvec",
"zune-inflate",
]
[[package]]
name = "fax"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a"
[[package]] [[package]]
name = "fdeflate" name = "fdeflate"
version = "0.3.7" version = "0.3.7"
@@ -74,6 +290,12 @@ dependencies = [
"simd-adler32", "simd-adler32",
] ]
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.1.9" version = "1.1.9"
@@ -84,6 +306,39 @@ dependencies = [
"miniz_oxide", "miniz_oxide",
] ]
[[package]]
name = "getrandom"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasip2",
]
[[package]]
name = "gif"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159"
dependencies = [
"color_quant",
"weezl",
]
[[package]]
name = "half"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
dependencies = [
"cfg-if",
"crunchy",
"zerocopy",
]
[[package]] [[package]]
name = "image" name = "image"
version = "0.25.10" version = "0.25.10"
@@ -92,9 +347,56 @@ checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"byteorder-lite", "byteorder-lite",
"color_quant",
"exr",
"gif",
"image-webp",
"moxcms", "moxcms",
"num-traits", "num-traits",
"png", "png 0.18.1",
"qoi",
"ravif",
"rayon",
"rgb",
"tiff",
"zune-core",
"zune-jpeg",
]
[[package]]
name = "image-webp"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3"
dependencies = [
"byteorder-lite",
"quick-error",
]
[[package]]
name = "imgref"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40fac9d56ed6437b198fddba683305e8e2d651aa42647f00f5ae542e7f5c94a2"
[[package]]
name = "interpolate_name"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "itertools"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
dependencies = [
"either",
] ]
[[package]] [[package]]
@@ -103,12 +405,63 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "jobserver"
version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
dependencies = [
"getrandom",
"libc",
]
[[package]]
name = "lebe"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8"
[[package]]
name = "libc"
version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "libfuzzer-sys"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d"
dependencies = [
"arbitrary",
"cc",
]
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.29" version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "loop9"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062"
dependencies = [
"imgref",
]
[[package]]
name = "maybe-rayon"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519"
dependencies = [
"cfg-if",
"rayon",
]
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.8.0" version = "2.8.0"
@@ -135,6 +488,77 @@ dependencies = [
"pxfm", "pxfm",
] ]
[[package]]
name = "new_debug_unreachable"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
[[package]]
name = "no_std_io2"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b51ed7824b6e07d354605f4abb3d9d300350701299da96642ee084f5ce631550"
dependencies = [
"memchr",
]
[[package]]
name = "nom"
version = "8.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
dependencies = [
"memchr",
]
[[package]]
name = "noop_proc_macro"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
[[package]]
name = "num-bigint"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
dependencies = [
"num-integer",
"num-traits",
]
[[package]]
name = "num-derive"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "num-integer"
version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
dependencies = [
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
dependencies = [
"num-bigint",
"num-integer",
"num-traits",
]
[[package]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.19" version = "0.2.19"
@@ -144,19 +568,59 @@ dependencies = [
"autocfg", "autocfg",
] ]
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pastey"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
[[package]]
name = "png"
version = "0.17.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
dependencies = [
"bitflags 1.3.2",
"crc32fast",
"fdeflate",
"flate2",
"miniz_oxide",
]
[[package]] [[package]]
name = "png" name = "png"
version = "0.18.1" version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.11.1",
"crc32fast", "crc32fast",
"fdeflate", "fdeflate",
"flate2", "flate2",
"miniz_oxide", "miniz_oxide",
] ]
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.106" version = "1.0.106"
@@ -166,12 +630,46 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "profiling"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773"
dependencies = [
"profiling-procmacros",
]
[[package]]
name = "profiling-procmacros"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b"
dependencies = [
"quote",
"syn",
]
[[package]] [[package]]
name = "pxfm" name = "pxfm"
version = "0.1.29" version = "0.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f"
[[package]]
name = "qoi"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001"
dependencies = [
"bytemuck",
]
[[package]]
name = "quick-error"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.45" version = "1.0.45"
@@ -181,6 +679,123 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "r-efi"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "rand"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
dependencies = [
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
dependencies = [
"getrandom",
]
[[package]]
name = "rav1e"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b"
dependencies = [
"aligned-vec",
"arbitrary",
"arg_enum_proc_macro",
"arrayvec",
"av-scenechange",
"av1-grain",
"bitstream-io",
"built",
"cfg-if",
"interpolate_name",
"itertools",
"libc",
"libfuzzer-sys",
"log",
"maybe-rayon",
"new_debug_unreachable",
"noop_proc_macro",
"num-derive",
"num-traits",
"paste",
"profiling",
"rand",
"rand_chacha",
"simd_helpers",
"thiserror",
"v_frame",
"wasm-bindgen",
]
[[package]]
name = "ravif"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45"
dependencies = [
"avif-serialize",
"imgref",
"loop9",
"quick-error",
"rav1e",
"rayon",
"rgb",
]
[[package]]
name = "rayon"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
]
[[package]]
name = "rgb"
version = "0.8.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4"
[[package]]
name = "rustversion"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.228" version = "1.0.228"
@@ -224,12 +839,39 @@ dependencies = [
"zmij", "zmij",
] ]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]] [[package]]
name = "simd-adler32" name = "simd-adler32"
version = "0.3.9" version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
[[package]]
name = "simd_helpers"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6"
dependencies = [
"quote",
]
[[package]]
name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "stable_deref_trait"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]] [[package]]
name = "strict-num" name = "strict-num"
version = "0.1.1" version = "0.1.1"
@@ -247,6 +889,40 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "thiserror"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tiff"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52"
dependencies = [
"fax",
"flate2",
"half",
"quick-error",
"weezl",
"zune-jpeg",
]
[[package]] [[package]]
name = "tiny-skia" name = "tiny-skia"
version = "0.11.4" version = "0.11.4"
@@ -258,6 +934,7 @@ dependencies = [
"bytemuck", "bytemuck",
"cfg-if", "cfg-if",
"log", "log",
"png 0.17.16",
"tiny-skia-path", "tiny-skia-path",
] ]
@@ -278,6 +955,109 @@ version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "v_frame"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2"
dependencies = [
"aligned-vec",
"num-traits",
"wasm-bindgen",
]
[[package]]
name = "wasip2"
version = "1.0.3+wasi-0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
dependencies = [
"wit-bindgen",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.120"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.120"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.120"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.120"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea"
dependencies = [
"unicode-ident",
]
[[package]]
name = "weezl"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
[[package]]
name = "wit-bindgen"
version = "0.57.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
[[package]]
name = "y4m"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448"
[[package]]
name = "zerocopy"
version = "0.8.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "zmij" name = "zmij"
version = "1.0.21" version = "1.0.21"
@@ -286,7 +1066,7 @@ checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
[[package]] [[package]]
name = "zshell-img-tools" name = "zshell-img-tools"
version = "0.2.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"image", "image",
@@ -294,3 +1074,27 @@ dependencies = [
"serde_json", "serde_json",
"tiny-skia", "tiny-skia",
] ]
[[package]]
name = "zune-core"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9"
[[package]]
name = "zune-inflate"
version = "0.2.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02"
dependencies = [
"simd-adler32",
]
[[package]]
name = "zune-jpeg"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296"
dependencies = [
"zune-core",
]
+4 -4
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "zshell-img-tools" name = "zshell-img-tools"
version = "0.2.0" version = "0.1.0"
edition = "2024" edition = "2024"
[[bin]] [[bin]]
@@ -8,10 +8,10 @@ name = "zshell-img-tools"
path = "src/main.rs" path = "src/main.rs"
[dependencies] [dependencies]
image = { version = "0.25", default-features = false, features = ["png"] } image = { version = "0.25", features = ["png"] }
tiny-skia = { version = "0.11", default-features = false, features = ["std", "simd"] } tiny-skia = "0.11"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
anyhow = "1.0" anyhow = "1"
serde_json = "1.0.149" serde_json = "1.0.149"
[profile.release] [profile.release]
+6
View File
@@ -0,0 +1,6 @@
# What_That_Claude_DO?
What That Claude Do? (WTCD)
A repository of random things I ask Claude to do for me.
In this case it is creating a screenshot tool
+2 -3
View File
@@ -11,14 +11,13 @@ pub struct Config {
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EffectsConfig { pub struct EffectsConfig {
pub mode: String, pub mode: String,
pub rounded_corners: bool,
pub corner_radius: f32, pub corner_radius: f32,
pub drop_shadow: bool, pub drop_shadow: bool,
pub rounded_corners: bool,
pub shadow_blur_radius: f32, pub shadow_blur_radius: f32,
pub shadow_blur_passes: u32,
pub shadow_color: [u8; 4],
pub shadow_offset_x: f32, pub shadow_offset_x: f32,
pub shadow_offset_y: f32, pub shadow_offset_y: f32,
pub shadow_color: [u8; 4],
} }
impl Config { impl Config {
+24 -37
View File
@@ -16,7 +16,6 @@ pub fn apply_effects(img: RgbaImage, cfg: &EffectsConfig) -> RgbaImage {
cfg.shadow_blur_radius, cfg.shadow_blur_radius,
cfg.shadow_offset_x, cfg.shadow_offset_x,
cfg.shadow_offset_y, cfg.shadow_offset_y,
cfg.shadow_blur_passes,
cfg.shadow_color, cfg.shadow_color,
) )
} else { } else {
@@ -53,16 +52,11 @@ pub fn apply_drop_shadow(
blur_radius: f32, blur_radius: f32,
offset_x: f32, offset_x: f32,
offset_y: f32, offset_y: f32,
blur_passes: u32,
shadow_color: [u8; 4], shadow_color: [u8; 4],
) -> RgbaImage { ) -> RgbaImage {
let (iw, ih) = img.dimensions(); let (iw, ih) = img.dimensions();
let br = blur_radius.ceil() as u32; let br = blur_radius.ceil() as u32;
let bp = blur_passes; let spread = br * 2;
// Original idea
// let spread = br * bp;
// Claude is hallucinating but let's try it **Worked btw**
let spread = (br as f32 * (bp as f32).sqrt() * 2.0).ceil() as u32;
let extra_left = spread + (-offset_x).max(0.0).ceil() as u32; let extra_left = spread + (-offset_x).max(0.0).ceil() as u32;
let extra_top = spread + (-offset_y).max(0.0).ceil() as u32; let extra_top = spread + (-offset_y).max(0.0).ceil() as u32;
@@ -92,11 +86,8 @@ pub fn apply_drop_shadow(
tint_pixmap_as_shadow(&mut shadow_pixmap, shadow_color); tint_pixmap_as_shadow(&mut shadow_pixmap, shadow_color);
// Shadow
let shadow_img = pixmap_to_rgba_image(shadow_pixmap); let shadow_img = pixmap_to_rgba_image(shadow_pixmap);
// Shadow blur let blurred = box_blur_rgba(&shadow_img, br);
let blurred = box_blur_rgba(&shadow_img, br, bp);
// Shadow pos
let blurred_pixmap = rgba_image_to_pixmap(&blurred); let blurred_pixmap = rgba_image_to_pixmap(&blurred);
let mut canvas = Pixmap::new(canvas_w, canvas_h).expect("canvas pixmap"); let mut canvas = Pixmap::new(canvas_w, canvas_h).expect("canvas pixmap");
@@ -145,7 +136,6 @@ fn rounded_rect_path(x: f32, y: f32, w: f32, h: f32, r: f32) -> Path {
pb.finish().expect("rounded rect path") pb.finish().expect("rounded rect path")
} }
// Shadow pos
fn rgba_image_to_pixmap(img: &RgbaImage) -> Pixmap { fn rgba_image_to_pixmap(img: &RgbaImage) -> Pixmap {
let (w, h) = img.dimensions(); let (w, h) = img.dimensions();
let mut pixmap = Pixmap::new(w, h).expect("pixmap alloc"); let mut pixmap = Pixmap::new(w, h).expect("pixmap alloc");
@@ -164,7 +154,6 @@ fn rgba_image_to_pixmap(img: &RgbaImage) -> Pixmap {
pixmap pixmap
} }
// Shadow
fn pixmap_to_rgba_image(pixmap: Pixmap) -> RgbaImage { fn pixmap_to_rgba_image(pixmap: Pixmap) -> RgbaImage {
let (w, h) = (pixmap.width(), pixmap.height()); let (w, h) = (pixmap.width(), pixmap.height());
let mut out = RgbaImage::new(w, h); let mut out = RgbaImage::new(w, h);
@@ -187,16 +176,31 @@ fn pixmap_to_rgba_image(pixmap: Pixmap) -> RgbaImage {
out out
} }
// Shadow blur fn tint_pixmap_as_shadow(pixmap: &mut Pixmap, color: [u8; 4]) {
fn box_blur_rgba(img: &RgbaImage, radius: u32, bp: u32) -> RgbaImage { let [sr, sg, sb, _] = color;
for px in pixmap.pixels_mut() {
let a = px.alpha();
if a > 0 {
let af = a as f32 / 255.0;
*px = tiny_skia::PremultipliedColorU8::from_rgba(
(sr as f32 * af) as u8,
(sg as f32 * af) as u8,
(sb as f32 * af) as u8,
a,
)
.unwrap_or(tiny_skia::PremultipliedColorU8::TRANSPARENT);
}
}
}
fn box_blur_rgba(img: &RgbaImage, radius: u32) -> RgbaImage {
if radius == 0 { if radius == 0 {
return img.clone(); return img.clone();
} }
let mut buf = img.clone(); let mut buf = sliding_horizontal(img, radius);
for _ in 0..bp { buf = sliding_vertical(&buf, radius);
buf = sliding_horizontal(&buf, radius); buf = sliding_horizontal(&buf, radius);
buf = sliding_vertical(&buf, radius); buf = sliding_vertical(&buf, radius);
}
buf buf
} }
@@ -246,23 +250,6 @@ fn sliding_horizontal(img: &RgbaImage, radius: u32) -> RgbaImage {
out out
} }
fn tint_pixmap_as_shadow(pixmap: &mut Pixmap, color: [u8; 4]) {
let [sr, sg, sb, _] = color;
for px in pixmap.pixels_mut() {
let a = px.alpha();
if a > 0 {
let af = a as f32 / 255.0;
*px = tiny_skia::PremultipliedColorU8::from_rgba(
(sr as f32 * af) as u8,
(sg as f32 * af) as u8,
(sb as f32 * af) as u8,
a,
)
.unwrap_or(tiny_skia::PremultipliedColorU8::TRANSPARENT);
}
}
}
fn sliding_vertical(img: &RgbaImage, radius: u32) -> RgbaImage { fn sliding_vertical(img: &RgbaImage, radius: u32) -> RgbaImage {
let (w, h) = img.dimensions(); let (w, h) = img.dimensions();
let r = radius as i32; let r = radius as i32;
+41 -58
View File
@@ -1,20 +1,21 @@
mod config; mod config;
mod effects; mod effects;
use anyhow::{bail, Context, Result}; use anyhow::{Context, Result, bail};
use std::io::Write as _; use std::io::Write as _;
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
/// CLI overrides that map 1:1 to `EffectsConfig` fields.
/// All fields are `Option<T>` so we can tell "not supplied" from any concrete value.
#[derive(Default)] #[derive(Default)]
struct CliOverrides { struct CliOverrides {
rounded_corners: Option<bool>, rounded_corners: Option<bool>,
corner_radius: Option<f32>, corner_radius: Option<f32>,
drop_shadow: Option<bool>, drop_shadow: Option<bool>,
shadow_blur_radius: Option<f32>, shadow_blur_radius: Option<f32>,
shadow_blur_passes: Option<u32>,
shadow_offset_x: Option<f32>, shadow_offset_x: Option<f32>,
shadow_offset_y: Option<f32>, shadow_offset_y: Option<f32>,
// Accepted as four comma-separated u8 values, e.g. `255,0,0,200` /// Accepted as four comma-separated u8 values, e.g. `255,0,0,200`
shadow_color: Option<[u8; 4]>, shadow_color: Option<[u8; 4]>,
} }
@@ -29,24 +30,24 @@ fn parse_bool(s: &str) -> Result<bool> {
fn parse_shadow_color(s: &str) -> Result<[u8; 4]> { fn parse_shadow_color(s: &str) -> Result<[u8; 4]> {
let parts: Vec<&str> = s.split(',').collect(); let parts: Vec<&str> = s.split(',').collect();
if parts.len() != 4 { if parts.len() != 4 {
bail!("--shadow-color expects four comma-separated u8 values, e.g. 255,0,0,200"); bail!("--shadow_color expects four comma-separated u8 values, e.g. 255,0,0,200");
} }
let r = parts[0] let r = parts[0]
.trim() .trim()
.parse::<u8>() .parse::<u8>()
.context("shadow-color red channel")?; .context("shadow_color red channel")?;
let g = parts[1] let g = parts[1]
.trim() .trim()
.parse::<u8>() .parse::<u8>()
.context("shadow-color green channel")?; .context("shadow_color green channel")?;
let b = parts[2] let b = parts[2]
.trim() .trim()
.parse::<u8>() .parse::<u8>()
.context("shadow-color blue channel")?; .context("shadow_color blue channel")?;
let a = parts[3] let a = parts[3]
.trim() .trim()
.parse::<u8>() .parse::<u8>()
.context("shadow-color alpha channel")?; .context("shadow_color alpha channel")?;
Ok([r, g, b, a]) Ok([r, g, b, a])
} }
@@ -55,7 +56,6 @@ fn main() -> Result<()> {
let mut image_path: Option<String> = None; let mut image_path: Option<String> = None;
let mut overrides = CliOverrides::default(); let mut overrides = CliOverrides::default();
let mut scale: Option<f32> = None;
let mut i = 0; let mut i = 0;
while i < args.len() { while i < args.len() {
@@ -68,82 +68,67 @@ fn main() -> Result<()> {
.context("Expected a path after --image")?, .context("Expected a path after --image")?,
); );
} }
"--rounded-corners" => { "--rounded_corners" => {
i += 1; i += 1;
let val = args let val = args
.get(i) .get(i)
.context("Expected true/false after --rounded-corners")?; .context("Expected true/false after --rounded_corners")?;
overrides.rounded_corners = Some(parse_bool(val)?); overrides.rounded_corners = Some(parse_bool(val)?);
} }
"--corner-radius" => { "--corner_radius" => {
i += 1; i += 1;
let val = args let val = args
.get(i) .get(i)
.context("Expected a number after --corner-radius")?; .context("Expected a number after --corner_radius")?;
overrides.corner_radius = Some( overrides.corner_radius = Some(
val.parse::<f32>() val.parse::<f32>()
.context("--corner-radius must be a number")?, .context("--corner_radius must be a number")?,
); );
} }
"--drop-shadow" => { "--drop_shadow" => {
i += 1; i += 1;
let val = args let val = args
.get(i) .get(i)
.context("Expected true/false after --drop-shadow")?; .context("Expected true/false after --drop_shadow")?;
overrides.drop_shadow = Some(parse_bool(val)?); overrides.drop_shadow = Some(parse_bool(val)?);
} }
"--shadow-blur-radius" => { "--shadow_blur_radius" => {
i += 1; i += 1;
let val = args let val = args
.get(i) .get(i)
.context("Expected a number after --shadow-blur-radius")?; .context("Expected a number after --shadow_blur_radius")?;
overrides.shadow_blur_radius = Some( overrides.shadow_blur_radius = Some(
val.parse::<f32>() val.parse::<f32>()
.context("--shadow-blur-radius must be a number")?, .context("--shadow_blur_radius must be a number")?,
); );
} }
"--shadow-offset-x" => { "--shadow_offset_x" => {
i += 1; i += 1;
let val = args let val = args
.get(i) .get(i)
.context("Expected a number after --shadow-offset-x")?; .context("Expected a number after --shadow_offset_x")?;
overrides.shadow_offset_x = Some( overrides.shadow_offset_x = Some(
val.parse::<f32>() val.parse::<f32>()
.context("--shadow-offset-x must be a number")?, .context("--shadow_offset_x must be a number")?,
); );
} }
"--shadow-offset-y" => { "--shadow_offset_y" => {
i += 1; i += 1;
let val = args let val = args
.get(i) .get(i)
.context("Expected a number after --shadow-offset-y")?; .context("Expected a number after --shadow_offset_y")?;
overrides.shadow_offset_y = Some( overrides.shadow_offset_y = Some(
val.parse::<f32>() val.parse::<f32>()
.context("--shadow-offset-y must be a number")?, .context("--shadow_offset_y must be a number")?,
); );
} }
"--shadow-blur-passes" => { "--shadow_color" => {
i += 1; i += 1;
let val = args let val = args
.get(i) .get(i)
.context("Expected a number after --shadow-blur-passes")?; .context("Expected r,g,b,a after --shadow_color")?;
overrides.shadow_blur_passes = Some(
val.parse::<u32>()
.context("--shadow-blur-passes must be a number")?,
);
}
"--shadow-color" => {
i += 1;
let val = args
.get(i)
.context("Expected r,g,b,a after --shadow-color")?;
overrides.shadow_color = Some(parse_shadow_color(val)?); overrides.shadow_color = Some(parse_shadow_color(val)?);
} }
"--scale" => {
i += 1;
let val = args.get(i).context("Expected a number after --scale")?;
scale = Some(val.parse::<f32>().context("--scale must be a number")?);
}
unknown => bail!("Unknown argument: {unknown}"), unknown => bail!("Unknown argument: {unknown}"),
} }
i += 1; i += 1;
@@ -173,22 +158,11 @@ fn main() -> Result<()> {
if let Some(v) = overrides.shadow_offset_y { if let Some(v) = overrides.shadow_offset_y {
effects.shadow_offset_y = v; effects.shadow_offset_y = v;
} }
if let Some(v) = overrides.shadow_blur_passes {
effects.shadow_blur_passes = v;
}
if let Some(v) = overrides.shadow_color { if let Some(v) = overrides.shadow_color {
effects.shadow_color = v; effects.shadow_color = v;
} }
} }
// if scale is set do
if let Some(scale) = scale.filter(|&s| s != 1.0) {
effects.corner_radius *= scale;
effects.shadow_blur_radius *= scale;
effects.shadow_offset_x *= scale;
effects.shadow_offset_y *= scale;
}
if let Err(e) = process_image(&image_path, &effects) { if let Err(e) = process_image(&image_path, &effects) {
eprintln!("Error processing '{}': {e:#}", image_path); eprintln!("Error processing '{}': {e:#}", image_path);
} }
@@ -217,11 +191,20 @@ fn process_image(path: &str, effects: &config::EffectsConfig) -> Result<()> {
.spawn() .spawn()
.context("Failed to spawn swappy. Is it installed and in PATH?")?; .context("Failed to spawn swappy. Is it installed and in PATH?")?;
// Writes the PNG bytes to swappy's stdin and then closes child
if let Some(mut stdin) = child.stdin.take() { .stdin
stdin .take()
.write_all(&png_bytes) .context("Failed to get swappy stdin")?
.context("Failed to write image data to swappy")?; .write_all(&png_bytes)
.context("Failed to write image data to swappy")?;
let status = child.wait().context("Failed to wait for swappy")?;
if !status.success() {
eprintln!(
"swappy exited with non-zero status for '{}': {}",
path, status
);
} }
Ok(()) Ok(())