Merge branch 'main' into forgejo-workflows
This commit is contained in:
@@ -12,3 +12,4 @@ pkg/
|
||||
uv.lock
|
||||
.qtcreator/
|
||||
dist/
|
||||
**/target/
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Config
|
||||
|
||||
IconButton {
|
||||
id: root
|
||||
|
||||
required property bool shouldBeVisible
|
||||
|
||||
opacity: 0
|
||||
scale: 0
|
||||
visible: root.scale > 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.small
|
||||
}
|
||||
}
|
||||
Behavior on scale {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
|
||||
onShouldBeVisibleChanged: {
|
||||
if (root.shouldBeVisible) {
|
||||
root.opacity = 1;
|
||||
root.scale = 1;
|
||||
} else {
|
||||
root.opacity = 0;
|
||||
root.scale = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
+19
-1
@@ -23,6 +23,7 @@ Singleton {
|
||||
property alias osd: adapter.osd
|
||||
property alias overview: adapter.overview
|
||||
property bool recentlySaved: false
|
||||
property alias screenshot: adapter.screenshot
|
||||
property alias services: adapter.services
|
||||
property alias sidebar: adapter.sidebar
|
||||
property alias utilities: adapter.utilities
|
||||
@@ -128,7 +129,8 @@ Singleton {
|
||||
background: serializeBackground(),
|
||||
launcher: serializeLauncher(),
|
||||
colors: serializeColors(),
|
||||
dock: serializeDock()
|
||||
dock: serializeDock(),
|
||||
screenshot: serializeScreenshot()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -275,6 +277,20 @@ Singleton {
|
||||
};
|
||||
}
|
||||
|
||||
function serializeScreenshot(): var {
|
||||
return {
|
||||
enable_pp: screenshot.enable_pp,
|
||||
mode: screenshot.mode,
|
||||
corner_radius: screenshot.corner_radius,
|
||||
drop_shadow: screenshot.drop_shadow,
|
||||
rounded_corners: screenshot.rounded_corners,
|
||||
shadow_blur_radius: screenshot.shadow_blur_radius,
|
||||
shadow_color: screenshot.shadow_color,
|
||||
shadow_offset_x: screenshot.shadow_offset_x,
|
||||
shadow_offset_y: screenshot.shadow_offset_y
|
||||
};
|
||||
}
|
||||
|
||||
function serializeServices(): var {
|
||||
return {
|
||||
weatherLocation: services.weatherLocation,
|
||||
@@ -430,6 +446,8 @@ Singleton {
|
||||
}
|
||||
property Overview overview: Overview {
|
||||
}
|
||||
property Screenshot screenshot: Screenshot {
|
||||
}
|
||||
property Services services: Services {
|
||||
}
|
||||
property SidebarConfig sidebar: SidebarConfig {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import Quickshell.Io
|
||||
|
||||
JsonObject {
|
||||
property real corner_radius: 12.0
|
||||
property bool drop_shadow: true
|
||||
property bool enable_pp: true
|
||||
property string mode: "manual"
|
||||
property bool rounded_corners: false
|
||||
property real shadow_blur_radius: 22.0
|
||||
property list<int> shadow_color: [0, 0, 0, 160]
|
||||
property real shadow_offset_x: 5.0
|
||||
property real shadow_offset_y: 5.0
|
||||
}
|
||||
+2
-1
@@ -66,7 +66,8 @@ MouseArea {
|
||||
|
||||
function save(): void {
|
||||
const tmpfile = Qt.resolvedUrl(`/tmp/zshell-picker-${Quickshell.processId}-${Date.now()}.png`);
|
||||
ZShellIo.saveItem(screencopy, tmpfile, Qt.rect(Math.ceil(rsx), Math.ceil(rsy), Math.floor(sw), Math.floor(sh)), path => Quickshell.execDetached(["swappy", "-f", path]));
|
||||
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]));
|
||||
closeAnim.start();
|
||||
}
|
||||
|
||||
|
||||
@@ -11,10 +11,13 @@ Singleton {
|
||||
id: root
|
||||
|
||||
property int availableUpdates: 0
|
||||
property string cmd: ""
|
||||
property bool commandReady
|
||||
property bool loaded
|
||||
property double now: Date.now()
|
||||
property var updates: ({})
|
||||
property bool updating
|
||||
property string updatingPackage: ""
|
||||
|
||||
function formatUpdateTime(timestamp) {
|
||||
const diffMs = root.now - timestamp;
|
||||
@@ -34,6 +37,22 @@ Singleton {
|
||||
return Qt.formatDateTime(new Date(timestamp), "dd hh:mm");
|
||||
}
|
||||
|
||||
function performPackageUpdate(pkg: string): void {
|
||||
if (root.cmd === "pacman")
|
||||
pkgUpdateProc.command = ["pkexec", root.cmd, "--noconfirm", "-Sy", pkg];
|
||||
else
|
||||
pkgUpdateProc.command = [root.cmd, "--noconfirm", "--sudo", "pkexec", "-Sy", pkg];
|
||||
pkgUpdateProc.running = true;
|
||||
}
|
||||
|
||||
function performSystemUpdate(): void {
|
||||
if (root.cmd === "pacman")
|
||||
sysUpdateProc.command = ["pkexec", root.cmd, "--noconfirm", "-Syu"];
|
||||
else
|
||||
sysUpdateProc.command = [root.cmd, "--noconfirm", "--sudo", "pkexec", "-Syu"];
|
||||
sysUpdateProc.running = true;
|
||||
}
|
||||
|
||||
onUpdatesChanged: {
|
||||
if (!root.loaded)
|
||||
return;
|
||||
@@ -92,6 +111,28 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: updateCmdDetect
|
||||
|
||||
command: ["sh", "-c", "command -v yay || command -v paru"]
|
||||
running: true
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const cmd = this.text.trim();
|
||||
let helper;
|
||||
|
||||
if (cmd.length > 0) {
|
||||
helper = cmd.split("/").pop();
|
||||
} else {
|
||||
helper = "pacman";
|
||||
}
|
||||
|
||||
root.cmd = helper;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: updatesProc
|
||||
|
||||
@@ -115,6 +156,44 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: sysUpdateProc
|
||||
|
||||
command: []
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
root.updating = false;
|
||||
}
|
||||
}
|
||||
|
||||
onRunningChanged: {
|
||||
if (running)
|
||||
root.updating = true;
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: pkgUpdateProc
|
||||
|
||||
command: []
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
root.updating = false;
|
||||
}
|
||||
}
|
||||
|
||||
onRunningChanged: {
|
||||
if (running) {
|
||||
root.updatingPackage = command[command.length - 1];
|
||||
root.updating = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: saveTimer
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ import qs.Helpers
|
||||
Scope {
|
||||
id: root
|
||||
|
||||
required property Lock lock
|
||||
readonly property bool enabled: !Players.list.some(p => p.isPlaying)
|
||||
required property Lock lock
|
||||
|
||||
function handleIdleAction(action: var): void {
|
||||
if (!action)
|
||||
@@ -19,6 +19,10 @@ Scope {
|
||||
lock.lock.locked = true;
|
||||
else if (action === "unlock")
|
||||
lock.lock.locked = false;
|
||||
else if (action === "dpms on")
|
||||
Hypr.dispatch('hl.dsp.dpms({ action = "enable" })');
|
||||
else if (action === "dpms off")
|
||||
Hypr.dispatch('hl.dsp.dpms({ action = "disable" })');
|
||||
else if (typeof action === "string")
|
||||
Hypr.dispatch(action);
|
||||
else
|
||||
@@ -33,6 +37,7 @@ Scope {
|
||||
|
||||
enabled: root.enabled && modelData.timeout > 0 ? true : false
|
||||
timeout: modelData.timeout
|
||||
|
||||
onIsIdleChanged: root.handleIdleAction(isIdle ? modelData.idleAction : modelData.activeAction)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,6 +104,18 @@ Item {
|
||||
key: "launcher"
|
||||
name: "Launcher"
|
||||
}
|
||||
|
||||
ListElement {
|
||||
icon: "screenshot_region"
|
||||
key: "screenshot"
|
||||
name: "Screenshot"
|
||||
}
|
||||
|
||||
ListElement {
|
||||
icon: "cached"
|
||||
key: "updates"
|
||||
name: "Updates"
|
||||
}
|
||||
}
|
||||
|
||||
CustomClippingRect {
|
||||
|
||||
@@ -22,6 +22,13 @@ ColumnLayout {
|
||||
Config.save();
|
||||
}
|
||||
|
||||
function deleteTimeoutEntry(index) {
|
||||
let list = [...Config.general.idle.timeouts];
|
||||
list.splice(index, 1);
|
||||
Config.general.idle.timeouts = list;
|
||||
Config.save();
|
||||
}
|
||||
|
||||
function updateTimeoutEntry(i, key, value) {
|
||||
const list = [...Config.general.idle.timeouts];
|
||||
let entry = list[i];
|
||||
@@ -49,6 +56,9 @@ ColumnLayout {
|
||||
onAddActiveActionRequested: {
|
||||
root.updateTimeoutEntry(index, "activeAction", "");
|
||||
}
|
||||
onDeleteRequested: function (index) {
|
||||
root.deleteTimeoutEntry(index);
|
||||
}
|
||||
onFieldEdited: function (key, value) {
|
||||
root.updateTimeoutEntry(index, key, value);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import qs.Modules.Settings.Controls
|
||||
import qs.Config
|
||||
import qs.Components
|
||||
|
||||
SettingsPage {
|
||||
SettingsSection {
|
||||
sectionId: "Screenshot"
|
||||
|
||||
SettingsHeader {
|
||||
name: "Screenshot"
|
||||
}
|
||||
|
||||
SettingSwitch {
|
||||
name: "Enable effects"
|
||||
object: Config.screenshot
|
||||
setting: "enable_pp"
|
||||
}
|
||||
|
||||
Separator {
|
||||
}
|
||||
|
||||
CustomSplitButtonRow {
|
||||
// active: true
|
||||
label: qsTr("Effects mode")
|
||||
|
||||
menuItems: [
|
||||
MenuItem {
|
||||
icon: "build"
|
||||
text: qsTr("Manual")
|
||||
value: "manual"
|
||||
},
|
||||
MenuItem {
|
||||
icon: "rotate_auto"
|
||||
text: qsTr("Auto")
|
||||
value: "auto"
|
||||
}
|
||||
]
|
||||
|
||||
onSelected: item => {
|
||||
Config.screenshot.mode = item.value;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
|
||||
Separator {
|
||||
visible: Config.screenshot.mode === "manual"
|
||||
}
|
||||
|
||||
SettingSpinBox {
|
||||
min: 0
|
||||
name: "Corner radius"
|
||||
object: Config.screenshot
|
||||
setting: "corner_radius"
|
||||
step: 1
|
||||
visible: Config.screenshot.mode === "manual"
|
||||
}
|
||||
|
||||
Separator {
|
||||
visible: Config.screenshot.mode === "manual"
|
||||
}
|
||||
|
||||
SettingSwitch {
|
||||
name: "Enable drop shadow"
|
||||
object: Config.screenshot
|
||||
setting: "drop_shadow"
|
||||
visible: Config.screenshot.mode === "manual"
|
||||
}
|
||||
|
||||
Separator {
|
||||
visible: Config.screenshot.mode === "manual"
|
||||
}
|
||||
|
||||
SettingSwitch {
|
||||
name: "Enable rounded corners"
|
||||
object: Config.screenshot
|
||||
setting: "rounded_corners"
|
||||
visible: Config.screenshot.mode === "manual"
|
||||
}
|
||||
|
||||
Separator {
|
||||
visible: Config.screenshot.mode === "manual"
|
||||
}
|
||||
|
||||
SettingSpinBox {
|
||||
min: 0
|
||||
name: "Shadow blur radius"
|
||||
object: Config.screenshot
|
||||
setting: "shadow_blur_radius"
|
||||
step: 1
|
||||
visible: Config.screenshot.mode === "manual"
|
||||
}
|
||||
|
||||
Separator {
|
||||
visible: Config.screenshot.mode === "manual"
|
||||
}
|
||||
|
||||
SettingSwitch {
|
||||
name: "Shadow color broken atm"
|
||||
object: Config.Screenshot
|
||||
setting: "shadow_color"
|
||||
visible: Config.screenshot.mode === "manual"
|
||||
}
|
||||
|
||||
Separator {
|
||||
visible: Config.screenshot.mode === "manual"
|
||||
}
|
||||
|
||||
SettingSpinBox {
|
||||
min: 0
|
||||
name: "Shadow offset X"
|
||||
object: Config.screenshot
|
||||
setting: "shadow_offset_x"
|
||||
step: 1
|
||||
visible: Config.screenshot.mode === "manual"
|
||||
}
|
||||
|
||||
Separator {
|
||||
visible: Config.screenshot.mode === "manual"
|
||||
}
|
||||
|
||||
SettingSpinBox {
|
||||
min: 0
|
||||
name: "Shadow offset Y"
|
||||
object: Config.screenshot
|
||||
setting: "shadow_offset_y"
|
||||
step: 1
|
||||
visible: Config.screenshot.mode === "manual"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import Quickshell
|
||||
import QtQuick.Layouts
|
||||
import QtQuick
|
||||
import qs.Config
|
||||
import qs.Helpers
|
||||
import qs.Components
|
||||
import qs.Modules.Settings.Controls
|
||||
|
||||
CustomClippingRect {
|
||||
id: root
|
||||
|
||||
radius: Appearance.rounding.normal - Appearance.padding.smaller
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: Appearance.padding.large
|
||||
spacing: Appearance.spacing.large
|
||||
|
||||
MaterialIcon {
|
||||
font.pointSize: Appearance.font.size.larger * 4
|
||||
text: "update"
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
CustomText {
|
||||
font.pointSize: Appearance.font.size.large * 2
|
||||
text: "System updates"
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: row
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
CustomText {
|
||||
id: text
|
||||
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
|
||||
Layout.fillWidth: true
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
text: `${Updates.availableUpdates} available updates`
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
Layout.preferredHeight: 40
|
||||
Layout.preferredWidth: 150
|
||||
color: Updates.updating ? DynamicColors.layer(DynamicColors.palette.m3outline, 2) : DynamicColors.palette.m3primary
|
||||
radius: Appearance.rounding.full
|
||||
|
||||
RowLayout {
|
||||
anchors.centerIn: parent
|
||||
|
||||
MaterialIcon {
|
||||
animate: true
|
||||
color: DynamicColors.palette.m3onPrimary
|
||||
font.pointSize: Appearance.font.size.large
|
||||
text: Updates.updating ? "update" : "download"
|
||||
}
|
||||
|
||||
CustomText {
|
||||
color: Updates.updating ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3onPrimary
|
||||
text: "Update all"
|
||||
}
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
color: DynamicColors.palette.m3onPrimary
|
||||
disabled: Updates.updating
|
||||
|
||||
onClicked: Updates.performSystemUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomListView {
|
||||
id: view
|
||||
|
||||
readonly property int itemHeight: 50 + Appearance.padding.smaller * 2
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
clip: true
|
||||
contentHeight: height
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
delegate: CustomRect {
|
||||
id: update
|
||||
|
||||
required property var modelData
|
||||
readonly property list<string> sections: modelData.update.split(" ")
|
||||
|
||||
color: DynamicColors.tPalette.m3surfaceContainer
|
||||
implicitHeight: view.itemHeight
|
||||
implicitWidth: parent.width
|
||||
radius: Appearance.rounding.small - Appearance.padding.small
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Appearance.padding.smaller
|
||||
anchors.rightMargin: Appearance.padding.smaller
|
||||
|
||||
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.preferredWidth: 500
|
||||
|
||||
MarqueeText {
|
||||
id: versionFrom
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.preferredWidth: 225
|
||||
animate: true
|
||||
color: DynamicColors.palette.m3tertiary
|
||||
font.pointSize: Appearance.font.size.large
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
marqueeEnabled: true
|
||||
pauseMs: 4000
|
||||
text: update.sections[1]
|
||||
width: 225
|
||||
}
|
||||
|
||||
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: 225
|
||||
animate: true
|
||||
color: DynamicColors.palette.m3primary
|
||||
font.pointSize: Appearance.font.size.large
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
marqueeEnabled: true
|
||||
pauseMs: 4000
|
||||
text: update.sections[3]
|
||||
width: 225
|
||||
}
|
||||
}
|
||||
|
||||
IconButton {
|
||||
Layout.preferredHeight: width
|
||||
icon: "download"
|
||||
|
||||
onClicked: {
|
||||
Updates.performPackageUpdate(update.sections[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
model: ScriptModel {
|
||||
id: script
|
||||
|
||||
objectProp: "update"
|
||||
values: Object.entries(Updates.updates).sort((a, b) => b[1] - a[1]).map(([update, timestamp]) => ({
|
||||
update,
|
||||
timestamp
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,6 +74,10 @@ Item {
|
||||
stack.push(osd);
|
||||
else if (currentCategory === "launcher")
|
||||
stack.push(launcher);
|
||||
else if (currentCategory === "screenshot")
|
||||
stack.push(screenshot);
|
||||
else if (currentCategory === "updates")
|
||||
stack.push(updates);
|
||||
}
|
||||
|
||||
target: root
|
||||
@@ -225,4 +229,18 @@ Item {
|
||||
Cat.Launcher {
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: screenshot
|
||||
|
||||
Cat.Screenshot {
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: updates
|
||||
|
||||
Cat.SystemUpdates {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ Item {
|
||||
required property var modelData
|
||||
|
||||
signal addActiveActionRequested
|
||||
signal deleteRequested(int index)
|
||||
signal fieldEdited(string key, var value)
|
||||
|
||||
Layout.fillWidth: true
|
||||
@@ -65,42 +66,64 @@ Item {
|
||||
|
||||
HoverHandler {
|
||||
id: nameHover
|
||||
}
|
||||
|
||||
HoverIconButton {
|
||||
id: editButton
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
font.pointSize: Appearance.font.size.large
|
||||
icon: "edit"
|
||||
shouldBeVisible: nameHover.hovered && !nameCell.editing
|
||||
|
||||
onClicked: nameCell.beginEdit()
|
||||
}
|
||||
|
||||
CustomText {
|
||||
anchors.left: parent.left
|
||||
anchors.right: editButton.left
|
||||
anchors.leftMargin: nameHover.hovered ? editButton.width + Appearance.spacing.smaller * 2 : 0
|
||||
anchors.right: deleteButton.left
|
||||
anchors.rightMargin: Appearance.spacing.small
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
elide: Text.ElideRight // enable if CustomText supports it
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
text: root.modelData.name
|
||||
visible: !nameCell.editing
|
||||
|
||||
Behavior on anchors.leftMargin {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IconButton {
|
||||
id: editButton
|
||||
HoverIconButton {
|
||||
id: deleteButton
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
font.pointSize: Appearance.font.size.large
|
||||
icon: "edit"
|
||||
visible: nameHover.hovered && !nameCell.editing
|
||||
icon: "delete"
|
||||
shouldBeVisible: nameHover.hovered && !nameCell.editing
|
||||
|
||||
onClicked: nameCell.beginEdit()
|
||||
onClicked: root.deleteRequested(root.index)
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
anchors.fill: parent
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: DynamicColors.tPalette.m3surface
|
||||
implicitHeight: nameEditor.implicitHeight + (Appearance.padding.normal * 2)
|
||||
implicitWidth: Math.min(nameEditor.contentWidth + (Appearance.padding.normal * 2), parent.width - Appearance.padding.normal)
|
||||
radius: Appearance.rounding.small
|
||||
visible: nameCell.editing
|
||||
|
||||
CustomTextField {
|
||||
id: nameEditor
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.centerIn: parent
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
implicitWidth: Math.min(contentWidth + Appearance.padding.normal * 2, nameCell.width - Appearance.padding.normal)
|
||||
text: nameCell.draftName
|
||||
|
||||
Keys.onEscapePressed: {
|
||||
|
||||
@@ -951,6 +951,72 @@ export const settingsIndex = [
|
||||
keywords: ["size", "osd", "height"],
|
||||
},
|
||||
|
||||
// SCREENSHOT CATEGORY
|
||||
// Screenshot section
|
||||
{
|
||||
name: "Enable effects",
|
||||
category: "screenshot",
|
||||
categoryName: "Screenshot",
|
||||
section: "Screenshot",
|
||||
keywords: ["effects", "shadow", "screenshot"],
|
||||
},
|
||||
{
|
||||
name: "Effects mode",
|
||||
category: "screenshot",
|
||||
categoryName: "Screenshot",
|
||||
section: "Screenshot",
|
||||
keywords: ["effects", "mode"],
|
||||
},
|
||||
{
|
||||
name: "Corner radius",
|
||||
category: "screenshot",
|
||||
categoryName: "Screenshot",
|
||||
section: "Screenshot",
|
||||
keywords: ["corner", "radius"],
|
||||
},
|
||||
{
|
||||
name: "Enable drop shadow",
|
||||
category: "screenshot",
|
||||
categoryName: "Screenshot",
|
||||
section: "Screenshot",
|
||||
keywords: ["drop", "shadow"],
|
||||
},
|
||||
{
|
||||
name: "Enable rounded corners",
|
||||
category: "screenshot",
|
||||
categoryName: "Screenshot",
|
||||
section: "Screenshot",
|
||||
keywords: ["rounded", "corners"],
|
||||
},
|
||||
{
|
||||
name: "Shadow blur radius",
|
||||
category: "screenshot",
|
||||
categoryName: "Screenshot",
|
||||
section: "Screenshot",
|
||||
keywords: ["blur", "shadow", "radius"],
|
||||
},
|
||||
{
|
||||
name: "Shadow color",
|
||||
category: "screenshot",
|
||||
categoryName: "Screenshot",
|
||||
section: "Screenshot",
|
||||
keywords: ["color", "shadow"],
|
||||
},
|
||||
{
|
||||
name: "Shadow offset X",
|
||||
category: "screenshot",
|
||||
categoryName: "Screenshot",
|
||||
section: "Screenshot",
|
||||
keywords: ["offset", "shadow"],
|
||||
},
|
||||
{
|
||||
name: "Shadow offset Y",
|
||||
category: "screenshot",
|
||||
categoryName: "Screenshot",
|
||||
section: "Screenshot",
|
||||
keywords: ["offset", "shadow"],
|
||||
},
|
||||
|
||||
// LAUNCHER CATEGORY
|
||||
// Launcher section
|
||||
{
|
||||
|
||||
Generated
+1100
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,27 @@
|
||||
[package]
|
||||
name = "zshell-img-tools"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[[bin]]
|
||||
name = "zshell-img-tools"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
image = { version = "0.25", features = ["png"] }
|
||||
tiny-skia = "0.11"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
anyhow = "1"
|
||||
serde_json = "1.0.149"
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
lto = "thin"
|
||||
codegen-units = 1
|
||||
strip = true
|
||||
|
||||
[profile.dev]
|
||||
opt-level = 0
|
||||
|
||||
[profile.dev.package."*"]
|
||||
opt-level = 3
|
||||
@@ -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
|
||||
@@ -0,0 +1,45 @@
|
||||
use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
#[serde(rename = "screenshot")]
|
||||
pub screenshot: EffectsConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EffectsConfig {
|
||||
pub mode: String,
|
||||
pub rounded_corners: bool,
|
||||
pub corner_radius: f32,
|
||||
pub drop_shadow: bool,
|
||||
pub shadow_blur_radius: f32,
|
||||
pub shadow_offset_x: f32,
|
||||
pub shadow_offset_y: f32,
|
||||
pub shadow_color: [u8; 4],
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn config_path() -> Option<PathBuf> {
|
||||
let home = std::env::var("HOME").ok()?;
|
||||
Some(
|
||||
PathBuf::from(home)
|
||||
.join(".config")
|
||||
.join("zshell")
|
||||
.join("config.json"),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn load() -> Result<Self> {
|
||||
let path = Self::config_path().context("Could not determine HOME directory")?;
|
||||
Self::load_from(&path)
|
||||
}
|
||||
|
||||
pub fn load_from(path: &PathBuf) -> Result<Self> {
|
||||
let raw = std::fs::read_to_string(path)
|
||||
.with_context(|| format!("Failed to read config at {}", path.display()))?;
|
||||
serde_json::from_str(&raw)
|
||||
.with_context(|| format!("Failed to parse JSON config at {}", path.display()))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
use crate::config::EffectsConfig;
|
||||
use image::RgbaImage;
|
||||
use tiny_skia::{
|
||||
BlendMode, Color, FillRule, Paint, Path, PathBuilder, Pixmap, PixmapPaint, Transform,
|
||||
};
|
||||
|
||||
pub fn apply_effects(img: RgbaImage, cfg: &EffectsConfig) -> RgbaImage {
|
||||
let img = if cfg.rounded_corners {
|
||||
apply_rounded_corners(img, cfg.corner_radius)
|
||||
} else {
|
||||
img
|
||||
};
|
||||
if cfg.drop_shadow {
|
||||
apply_drop_shadow(
|
||||
img,
|
||||
cfg.shadow_blur_radius,
|
||||
cfg.shadow_offset_x,
|
||||
cfg.shadow_offset_y,
|
||||
cfg.shadow_color,
|
||||
)
|
||||
} else {
|
||||
img
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_rounded_corners(img: RgbaImage, radius: f32) -> RgbaImage {
|
||||
let (w, h) = img.dimensions();
|
||||
let mut mask = Pixmap::new(w, h).expect("mask pixmap");
|
||||
let path = rounded_rect_path(0.0, 0.0, w as f32, h as f32, radius);
|
||||
let mut paint = Paint::default();
|
||||
paint.set_color(Color::WHITE);
|
||||
paint.anti_alias = true;
|
||||
mask.fill_path(
|
||||
&path,
|
||||
&paint,
|
||||
FillRule::Winding,
|
||||
Transform::identity(),
|
||||
None,
|
||||
);
|
||||
|
||||
let mut pixmap = rgba_image_to_pixmap(&img);
|
||||
let mut dst_paint = PixmapPaint::default();
|
||||
dst_paint.blend_mode = BlendMode::DestinationIn;
|
||||
pixmap.draw_pixmap(0, 0, mask.as_ref(), &dst_paint, Transform::identity(), None);
|
||||
pixmap_to_rgba_image(pixmap)
|
||||
}
|
||||
|
||||
pub fn apply_drop_shadow(
|
||||
img: RgbaImage,
|
||||
blur_radius: f32,
|
||||
offset_x: f32,
|
||||
offset_y: f32,
|
||||
shadow_color: [u8; 4],
|
||||
) -> RgbaImage {
|
||||
let (iw, ih) = img.dimensions();
|
||||
let br = blur_radius.ceil() as u32;
|
||||
let spread = br * 2;
|
||||
|
||||
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_right = spread + offset_x.max(0.0).ceil() as u32;
|
||||
let extra_bottom = spread + offset_y.max(0.0).ceil() as u32;
|
||||
|
||||
let canvas_w = iw + extra_left + extra_right;
|
||||
let canvas_h = ih + extra_top + extra_bottom;
|
||||
|
||||
let mut shadow_pixmap = Pixmap::new(canvas_w, canvas_h).expect("shadow pixmap");
|
||||
let img_pixmap = rgba_image_to_pixmap(&img);
|
||||
let shadow_x = (extra_left as f32 + offset_x) as i32;
|
||||
let shadow_y = (extra_top as f32 + offset_y) as i32;
|
||||
|
||||
let mut sp = PixmapPaint::default();
|
||||
sp.blend_mode = BlendMode::Source;
|
||||
shadow_pixmap.draw_pixmap(
|
||||
shadow_x,
|
||||
shadow_y,
|
||||
img_pixmap.as_ref(),
|
||||
&sp,
|
||||
Transform::identity(),
|
||||
None,
|
||||
);
|
||||
|
||||
tint_pixmap_as_shadow(&mut shadow_pixmap, shadow_color);
|
||||
|
||||
let shadow_img = pixmap_to_rgba_image(shadow_pixmap);
|
||||
let blurred = box_blur_rgba(&shadow_img, br);
|
||||
let blurred_pixmap = rgba_image_to_pixmap(&blurred);
|
||||
|
||||
let mut canvas = Pixmap::new(canvas_w, canvas_h).expect("canvas pixmap");
|
||||
let mut p = PixmapPaint::default();
|
||||
p.blend_mode = BlendMode::Source;
|
||||
canvas.draw_pixmap(
|
||||
0,
|
||||
0,
|
||||
blurred_pixmap.as_ref(),
|
||||
&p,
|
||||
Transform::identity(),
|
||||
None,
|
||||
);
|
||||
|
||||
let mut p2 = PixmapPaint::default();
|
||||
p2.blend_mode = BlendMode::SourceOver;
|
||||
canvas.draw_pixmap(
|
||||
extra_left as i32,
|
||||
extra_top as i32,
|
||||
img_pixmap.as_ref(),
|
||||
&p2,
|
||||
Transform::identity(),
|
||||
None,
|
||||
);
|
||||
|
||||
pixmap_to_rgba_image(canvas)
|
||||
}
|
||||
|
||||
fn rounded_rect_path(x: f32, y: f32, w: f32, h: f32, r: f32) -> Path {
|
||||
let r = r.min(w / 2.0).min(h / 2.0);
|
||||
let mut pb = PathBuilder::new();
|
||||
pb.move_to(x + r, y);
|
||||
pb.line_to(x + w - r, y);
|
||||
pb.quad_to(x + w, y, x + w, y + r);
|
||||
pb.line_to(x + w, y + h - r);
|
||||
pb.quad_to(x + w, y + h, x + w - r, y + h);
|
||||
pb.line_to(x + r, y + h);
|
||||
pb.quad_to(x, y + h, x, y + h - r);
|
||||
pb.line_to(x, y + r);
|
||||
pb.quad_to(x, y, x + r, y);
|
||||
pb.close();
|
||||
pb.finish().expect("rounded rect path")
|
||||
}
|
||||
|
||||
fn rgba_image_to_pixmap(img: &RgbaImage) -> Pixmap {
|
||||
let (w, h) = img.dimensions();
|
||||
let mut pixmap = Pixmap::new(w, h).expect("pixmap alloc");
|
||||
let pixels = pixmap.pixels_mut();
|
||||
for (i, px) in img.pixels().enumerate() {
|
||||
let [r, g, b, a] = px.0;
|
||||
let af = a as f32 / 255.0;
|
||||
pixels[i] = tiny_skia::PremultipliedColorU8::from_rgba(
|
||||
(r as f32 * af) as u8,
|
||||
(g as f32 * af) as u8,
|
||||
(b as f32 * af) as u8,
|
||||
a,
|
||||
)
|
||||
.unwrap_or(tiny_skia::PremultipliedColorU8::TRANSPARENT);
|
||||
}
|
||||
pixmap
|
||||
}
|
||||
|
||||
fn pixmap_to_rgba_image(pixmap: Pixmap) -> RgbaImage {
|
||||
let (w, h) = (pixmap.width(), pixmap.height());
|
||||
let mut out = RgbaImage::new(w, h);
|
||||
for (i, px) in pixmap.pixels().iter().enumerate() {
|
||||
let x = (i as u32) % w;
|
||||
let y = (i as u32) / w;
|
||||
let a = px.alpha();
|
||||
let (r, g, b) = if a == 0 {
|
||||
(0, 0, 0)
|
||||
} else {
|
||||
let af = a as f32 / 255.0;
|
||||
(
|
||||
(px.red() as f32 / af).round().min(255.0) as u8,
|
||||
(px.green() as f32 / af).round().min(255.0) as u8,
|
||||
(px.blue() as f32 / af).round().min(255.0) as u8,
|
||||
)
|
||||
};
|
||||
out.put_pixel(x, y, image::Rgba([r, g, b, a]));
|
||||
}
|
||||
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 box_blur_rgba(img: &RgbaImage, radius: u32) -> RgbaImage {
|
||||
if radius == 0 {
|
||||
return img.clone();
|
||||
}
|
||||
let mut buf = sliding_horizontal(img, radius);
|
||||
buf = sliding_vertical(&buf, radius);
|
||||
buf = sliding_horizontal(&buf, radius);
|
||||
buf = sliding_vertical(&buf, radius);
|
||||
buf
|
||||
}
|
||||
|
||||
fn sliding_horizontal(img: &RgbaImage, radius: u32) -> RgbaImage {
|
||||
let (w, h) = img.dimensions();
|
||||
let r = radius as i32;
|
||||
let diam = (2 * r + 1) as u32;
|
||||
let mut out = RgbaImage::new(w, h);
|
||||
|
||||
for y in 0..h {
|
||||
let mut sr = 0u32;
|
||||
let mut sg = 0u32;
|
||||
let mut sb = 0u32;
|
||||
let mut sa = 0u32;
|
||||
|
||||
for dx in -r..=r {
|
||||
let sx = dx.clamp(0, w as i32 - 1) as u32;
|
||||
let p = img.get_pixel(sx, y).0;
|
||||
sr += p[0] as u32;
|
||||
sg += p[1] as u32;
|
||||
sb += p[2] as u32;
|
||||
sa += p[3] as u32;
|
||||
}
|
||||
|
||||
for x in 0..w {
|
||||
out.put_pixel(
|
||||
x,
|
||||
y,
|
||||
image::Rgba([
|
||||
(sr / diam) as u8,
|
||||
(sg / diam) as u8,
|
||||
(sb / diam) as u8,
|
||||
(sa / diam) as u8,
|
||||
]),
|
||||
);
|
||||
|
||||
let remove_x = (x as i32 - r).clamp(0, w as i32 - 1) as u32;
|
||||
let add_x = (x as i32 + r + 1).clamp(0, w as i32 - 1) as u32;
|
||||
let rp = img.get_pixel(remove_x, y).0;
|
||||
let ap = img.get_pixel(add_x, y).0;
|
||||
sr = sr.saturating_sub(rp[0] as u32) + ap[0] as u32;
|
||||
sg = sg.saturating_sub(rp[1] as u32) + ap[1] as u32;
|
||||
sb = sb.saturating_sub(rp[2] as u32) + ap[2] as u32;
|
||||
sa = sa.saturating_sub(rp[3] as u32) + ap[3] as u32;
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn sliding_vertical(img: &RgbaImage, radius: u32) -> RgbaImage {
|
||||
let (w, h) = img.dimensions();
|
||||
let r = radius as i32;
|
||||
let diam = (2 * r + 1) as u32;
|
||||
let mut out = RgbaImage::new(w, h);
|
||||
|
||||
for x in 0..w {
|
||||
let mut sr = 0u32;
|
||||
let mut sg = 0u32;
|
||||
let mut sb = 0u32;
|
||||
let mut sa = 0u32;
|
||||
|
||||
for dy in -r..=r {
|
||||
let sy = dy.clamp(0, h as i32 - 1) as u32;
|
||||
let p = img.get_pixel(x, sy).0;
|
||||
sr += p[0] as u32;
|
||||
sg += p[1] as u32;
|
||||
sb += p[2] as u32;
|
||||
sa += p[3] as u32;
|
||||
}
|
||||
|
||||
for y in 0..h {
|
||||
out.put_pixel(
|
||||
x,
|
||||
y,
|
||||
image::Rgba([
|
||||
(sr / diam) as u8,
|
||||
(sg / diam) as u8,
|
||||
(sb / diam) as u8,
|
||||
(sa / diam) as u8,
|
||||
]),
|
||||
);
|
||||
|
||||
let remove_y = (y as i32 - r).clamp(0, h as i32 - 1) as u32;
|
||||
let add_y = (y as i32 + r + 1).clamp(0, h as i32 - 1) as u32;
|
||||
let rp = img.get_pixel(x, remove_y).0;
|
||||
let ap = img.get_pixel(x, add_y).0;
|
||||
sr = sr.saturating_sub(rp[0] as u32) + ap[0] as u32;
|
||||
sg = sg.saturating_sub(rp[1] as u32) + ap[1] as u32;
|
||||
sb = sb.saturating_sub(rp[2] as u32) + ap[2] as u32;
|
||||
sa = sa.saturating_sub(rp[3] as u32) + ap[3] as u32;
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
mod config;
|
||||
mod effects;
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use std::io::Write as _;
|
||||
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)]
|
||||
struct CliOverrides {
|
||||
rounded_corners: Option<bool>,
|
||||
corner_radius: Option<f32>,
|
||||
drop_shadow: Option<bool>,
|
||||
shadow_blur_radius: Option<f32>,
|
||||
shadow_offset_x: Option<f32>,
|
||||
shadow_offset_y: Option<f32>,
|
||||
/// Accepted as four comma-separated u8 values, e.g. `255,0,0,200`
|
||||
shadow_color: Option<[u8; 4]>,
|
||||
}
|
||||
|
||||
fn parse_bool(s: &str) -> Result<bool> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"true" | "1" | "yes" => Ok(true),
|
||||
"false" | "0" | "no" => Ok(false),
|
||||
other => bail!("Expected a boolean (true/false), got '{other}'"),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_shadow_color(s: &str) -> Result<[u8; 4]> {
|
||||
let parts: Vec<&str> = s.split(',').collect();
|
||||
if parts.len() != 4 {
|
||||
bail!("--shadow_color expects four comma-separated u8 values, e.g. 255,0,0,200");
|
||||
}
|
||||
let r = parts[0]
|
||||
.trim()
|
||||
.parse::<u8>()
|
||||
.context("shadow_color red channel")?;
|
||||
let g = parts[1]
|
||||
.trim()
|
||||
.parse::<u8>()
|
||||
.context("shadow_color green channel")?;
|
||||
let b = parts[2]
|
||||
.trim()
|
||||
.parse::<u8>()
|
||||
.context("shadow_color blue channel")?;
|
||||
let a = parts[3]
|
||||
.trim()
|
||||
.parse::<u8>()
|
||||
.context("shadow_color alpha channel")?;
|
||||
Ok([r, g, b, a])
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
|
||||
let mut image_path: Option<String> = None;
|
||||
let mut overrides = CliOverrides::default();
|
||||
|
||||
let mut i = 0;
|
||||
while i < args.len() {
|
||||
match args[i].as_str() {
|
||||
"--image" => {
|
||||
i += 1;
|
||||
image_path = Some(
|
||||
args.get(i)
|
||||
.cloned()
|
||||
.context("Expected a path after --image")?,
|
||||
);
|
||||
}
|
||||
"--rounded_corners" => {
|
||||
i += 1;
|
||||
let val = args
|
||||
.get(i)
|
||||
.context("Expected true/false after --rounded_corners")?;
|
||||
overrides.rounded_corners = Some(parse_bool(val)?);
|
||||
}
|
||||
"--corner_radius" => {
|
||||
i += 1;
|
||||
let val = args
|
||||
.get(i)
|
||||
.context("Expected a number after --corner_radius")?;
|
||||
overrides.corner_radius = Some(
|
||||
val.parse::<f32>()
|
||||
.context("--corner_radius must be a number")?,
|
||||
);
|
||||
}
|
||||
"--drop_shadow" => {
|
||||
i += 1;
|
||||
let val = args
|
||||
.get(i)
|
||||
.context("Expected true/false after --drop_shadow")?;
|
||||
overrides.drop_shadow = Some(parse_bool(val)?);
|
||||
}
|
||||
"--shadow_blur_radius" => {
|
||||
i += 1;
|
||||
let val = args
|
||||
.get(i)
|
||||
.context("Expected a number after --shadow_blur_radius")?;
|
||||
overrides.shadow_blur_radius = Some(
|
||||
val.parse::<f32>()
|
||||
.context("--shadow_blur_radius must be a number")?,
|
||||
);
|
||||
}
|
||||
"--shadow_offset_x" => {
|
||||
i += 1;
|
||||
let val = args
|
||||
.get(i)
|
||||
.context("Expected a number after --shadow_offset_x")?;
|
||||
overrides.shadow_offset_x = Some(
|
||||
val.parse::<f32>()
|
||||
.context("--shadow_offset_x must be a number")?,
|
||||
);
|
||||
}
|
||||
"--shadow_offset_y" => {
|
||||
i += 1;
|
||||
let val = args
|
||||
.get(i)
|
||||
.context("Expected a number after --shadow_offset_y")?;
|
||||
overrides.shadow_offset_y = Some(
|
||||
val.parse::<f32>()
|
||||
.context("--shadow_offset_y 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)?);
|
||||
}
|
||||
unknown => bail!("Unknown argument: {unknown}"),
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
let image_path = image_path.context("Missing --image <path>")?;
|
||||
|
||||
let config = config::Config::load().context("Failed to load config")?;
|
||||
|
||||
let mut effects = config.screenshot;
|
||||
if effects.mode == "auto" {
|
||||
if let Some(v) = overrides.rounded_corners {
|
||||
effects.rounded_corners = v;
|
||||
}
|
||||
if let Some(v) = overrides.corner_radius {
|
||||
effects.corner_radius = v;
|
||||
}
|
||||
if let Some(v) = overrides.drop_shadow {
|
||||
effects.drop_shadow = v;
|
||||
}
|
||||
if let Some(v) = overrides.shadow_blur_radius {
|
||||
effects.shadow_blur_radius = v;
|
||||
}
|
||||
if let Some(v) = overrides.shadow_offset_x {
|
||||
effects.shadow_offset_x = v;
|
||||
}
|
||||
if let Some(v) = overrides.shadow_offset_y {
|
||||
effects.shadow_offset_y = v;
|
||||
}
|
||||
if let Some(v) = overrides.shadow_color {
|
||||
effects.shadow_color = v;
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = process_image(&image_path, &effects) {
|
||||
eprintln!("Error processing '{}': {e:#}", image_path);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn process_image(path: &str, effects: &config::EffectsConfig) -> Result<()> {
|
||||
let img = image::open(path)
|
||||
.with_context(|| format!("Failed to open image '{path}'"))?
|
||||
.into_rgba8();
|
||||
|
||||
let processed = effects::apply_effects(img, effects);
|
||||
|
||||
let mut png_bytes: Vec<u8> = Vec::new();
|
||||
image::DynamicImage::ImageRgba8(processed)
|
||||
.write_to(
|
||||
&mut std::io::Cursor::new(&mut png_bytes),
|
||||
image::ImageFormat::Png,
|
||||
)
|
||||
.context("Failed to encode processed image as PNG")?;
|
||||
|
||||
let mut child = Command::new("swappy")
|
||||
.args(["-f", "-"])
|
||||
.stdin(Stdio::piped())
|
||||
.spawn()
|
||||
.context("Failed to spawn swappy. Is it installed and in PATH?")?;
|
||||
|
||||
child
|
||||
.stdin
|
||||
.take()
|
||||
.context("Failed to get swappy stdin")?
|
||||
.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(())
|
||||
}
|
||||
Reference in New Issue
Block a user