waffles: screen snip

This commit is contained in:
end-4
2025-12-20 11:46:07 +01:00
parent 169b24bea5
commit 8842df6340
34 changed files with 976 additions and 113 deletions
@@ -0,0 +1,81 @@
pragma ComponentBehavior: Bound
pragma Singleton
import qs.modules.common
import qs.modules.common.utils
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Controls
import Qt.labs.synchronizer
import Quickshell
Singleton {
id: root
enum Action {
Copy,
Edit,
Search,
CharRecognition,
Record,
RecordWithSound
}
property string imageSearchEngineBaseUrl: Config.options.search.imageSearch.imageSearchEngineBaseUrl
property string fileUploadApiEndpoint: "https://uguu.se/upload"
function getCommand(x, y, width, height, screenshotPath, action, saveDir = "") {
// Set command for action
const rx = Math.round(x);
const ry = Math.round(y);
const rw = Math.round(width);
const rh = Math.round(height);
const cropBase = `magick ${StringUtils.shellSingleQuoteEscape(screenshotPath)} `
+ `-crop ${rw}x${rh}+${rx}+${ry}`
const cropToStdout = `${cropBase} -`
const cropInPlace = `${cropBase} '${StringUtils.shellSingleQuoteEscape(screenshotPath)}'`
const cleanup = `rm '${StringUtils.shellSingleQuoteEscape(screenshotPath)}'`
const slurpRegion = `${rx},${ry} ${rw}x${rh}`
const uploadAndGetUrl = (filePath) => {
return `curl -sF files[]=@'${StringUtils.shellSingleQuoteEscape(filePath)}' ${root.fileUploadApiEndpoint} | jq -r '.files[0].url'`
}
const annotationCommand = `${Config.options.regionSelector.annotation.useSatty ? "satty" : "swappy"} -f -`;
switch (action) {
case ScreenshotAction.Action.Copy:
if (saveDir === "") {
// not saving the screenshot, just copy to clipboard
return ["bash", "-c", `${cropToStdout} | wl-copy && ${cleanup}`]
break;
}
return [
"bash", "-c",
`mkdir -p '${StringUtils.shellSingleQuoteEscape(saveDir)}' && \
saveFileName="screenshot-$(date '+%Y-%m-%d_%H.%M.%S').png" && \
savePath="${saveDir}/$saveFileName" && \
${cropToStdout} | tee >(wl-copy) > "$savePath" && \
${cleanup}`
]
break;
case ScreenshotAction.Action.Edit:
return ["bash", "-c", `${cropToStdout} | ${annotationCommand} && ${cleanup}`]
break;
case ScreenshotAction.Action.Search:
return ["bash", "-c", `${cropInPlace} && xdg-open "${root.imageSearchEngineBaseUrl}$(${uploadAndGetUrl(screenshotPath)})" && ${cleanup}`]
break;
case ScreenshotAction.Action.CharRecognition:
return ["bash", "-c", `${cropInPlace} && tesseract '${StringUtils.shellSingleQuoteEscape(screenshotPath)}' stdout -l $(tesseract --list-langs | awk 'NR>1{print $1}' | tr '\\n' '+' | sed 's/\\+$/\\n/') | wl-copy && ${cleanup}`]
break;
case ScreenshotAction.Action.Record:
return ["bash", "-c", `${Directories.recordScriptPath} --region '${slurpRegion}'`]
break;
case ScreenshotAction.Action.RecordWithSound:
return ["bash", "-c", `${Directories.recordScriptPath} --region '${slurpRegion}' --sound`]
break;
default:
console.warn("[Region Selector] Unknown snip action, skipping snip.");
return;
}
}
}
@@ -0,0 +1,14 @@
import QtQuick
import Quickshell
import Quickshell.Io
import qs.modules.common
import qs.modules.common.functions
Process {
id: screenshotProc
running: true
property string screenshotDir: Directories.screenshotTemp
required property ShellScreen screen
property string screenshotPath: `${screenshotDir}/image-${screen.name}`
command: ["bash", "-c", `mkdir -p '${StringUtils.shellSingleQuoteEscape(screenshotDir)}' && grim -o '${StringUtils.shellSingleQuoteEscape(screen.name)}' '${StringUtils.shellSingleQuoteEscape(screenshotPath)}'`]
}
@@ -0,0 +1,28 @@
import QtQuick
import qs.modules.common
import qs.modules.common.functions
Canvas {
id: root
property color color: "#ffffff"
property int dashLength: 6
property int gapLength: 4
property int borderWidth: 1
onDashLengthChanged: requestPaint()
onGapLengthChanged: requestPaint()
onWidthChanged: requestPaint()
onHeightChanged: requestPaint()
onPaint: {
var ctx = getContext("2d");
ctx.clearRect(0, 0, width, height);
ctx.save();
ctx.strokeStyle = root.color;
ctx.lineWidth = root.borderWidth;
if (root.gapLength > 0) {
ctx.setLineDash([root.dashLength, root.gapLength]); // Set dash pattern
}
ctx.strokeRect(root.borderWidth / 2, root.borderWidth / 2, width - root.borderWidth, height - root.borderWidth); // Draw it
ctx.restore();
}
}
@@ -14,12 +14,16 @@ MouseArea {
property bool automaticallyReset: true
readonly property real dragDiffX: _dragDiffX
readonly property real dragDiffY: _dragDiffY
property real startX: 0
property real startY: 0
property real regionTopLeftX: Math.min(startX, startX + _dragDiffX)
property real regionTopLeftY: Math.min(startY, startY + _dragDiffY)
property real regionWidth: Math.abs(_dragDiffX)
property real regionHeight: Math.abs(_dragDiffY)
signal dragPressed(diffX: real, diffY: real)
signal dragReleased(diffX: real, diffY: real)
property real startX: 0
property real startY: 0
property bool dragging: false
property real _dragDiffX: 0
property real _dragDiffY: 0
@@ -12,19 +12,30 @@ Item {
required property var tabButtonList
function incrementCurrentIndex() {
tabBar.incrementCurrentIndex()
tabBar.incrementCurrentIndex();
}
function decrementCurrentIndex() {
tabBar.decrementCurrentIndex()
tabBar.decrementCurrentIndex();
}
function setCurrentIndex(index) {
tabBar.setCurrentIndex(index)
tabBar.setCurrentIndex(index);
}
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
implicitWidth: contentItem.implicitWidth
implicitHeight: 40
property Component delegate: ToolbarTabButton {
required property int index
required property var modelData
current: index == root.currentIndex
text: modelData.name
materialSymbol: modelData.icon
onClicked: {
root.setCurrentIndex(index);
}
}
Row {
id: contentItem
z: 1
@@ -33,16 +44,7 @@ Item {
Repeater {
model: root.tabButtonList
delegate: ToolbarTabButton {
required property int index
required property var modelData
current: index == root.currentIndex
text: modelData.name
materialSymbol: modelData.icon
onClicked: {
root.setCurrentIndex(index)
}
}
delegate: root.delegate
}
}
@@ -76,23 +78,23 @@ Item {
z: 2
acceptedButtons: Qt.NoButton
cursorShape: Qt.PointingHandCursor
onWheel: (event) => {
onWheel: event => {
if (event.angleDelta.y < 0) {
root.incrementCurrentIndex();
}
else {
} else {
root.decrementCurrentIndex();
}
}
}
// TabBar doesn't allow tabs to be of different sizes. Literally unusable.
// TabBar doesn't allow tabs to be of different sizes. That's what I thought...
// We use it only for the logic and draw stuff manually
TabBar {
id: tabBar
z: -1
background: null
Repeater { // This is to fool the TabBar that it has tabs so it does the indices properly
Repeater {
// This is to fool the TabBar that it has tabs so it does the indices properly
model: root.tabButtonList.length
delegate: TabButton {
background: null