mirror of
https://github.com/belsabbagh/dotfiles.git
synced 2026-04-11 09:36:46 +00:00
607 lines
24 KiB
QML
607 lines
24 KiB
QML
pragma ComponentBehavior: Bound
|
|
import Quickshell
|
|
import Quickshell.Hyprland
|
|
import Quickshell.Io
|
|
import QtQuick
|
|
import QtQuick.Layouts
|
|
import QtQuick.Effects
|
|
import Quickshell.Wayland
|
|
import qs.config
|
|
import qs.modules.components
|
|
|
|
Scope {
|
|
id: root
|
|
property bool active: false
|
|
property rect selectedRegion: Qt.rect(0, 0, 0, 0)
|
|
property string tempScreenshot: ""
|
|
|
|
IpcHandler {
|
|
target: "screen"
|
|
function capture() {
|
|
if (root.active) {
|
|
console.info("screencap", "already active");
|
|
return;
|
|
}
|
|
console.info("screencap", "starting capture");
|
|
root.active = true;
|
|
}
|
|
}
|
|
|
|
LazyLoader {
|
|
active: root.active
|
|
component: PanelWindow {
|
|
id: win
|
|
property bool closing: false
|
|
property bool ready: false
|
|
property bool processing: false
|
|
property bool windowMode: false
|
|
property string savedPath: ""
|
|
property bool savedSuccess: false
|
|
|
|
color: Appearance.m3colors.m3surface
|
|
anchors { top: true; left: true; right: true; bottom: true }
|
|
WlrLayershell.layer: WlrLayer.Overlay
|
|
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
|
WlrLayershell.namespace: "nucleus:screencapture"
|
|
|
|
Component.onCompleted: {
|
|
var ts = Qt.formatDateTime(new Date(), "yyyy-MM-dd_hh-mm-ss");
|
|
root.tempScreenshot = "/tmp/screenshot_" + ts + ".png";
|
|
}
|
|
|
|
function close() {
|
|
if (closing) return;
|
|
closing = true;
|
|
closeAnim.start();
|
|
}
|
|
|
|
function saveFullscreen() {
|
|
console.info("screencap", "saveFullscreen started");
|
|
win.processing = true;
|
|
screencopy.grabToImage(function(result) {
|
|
console.info("screencap", "fullscreen grabbed");
|
|
var ts = Qt.formatDateTime(new Date(), "yyyy-MM-dd_hh-mm-ss");
|
|
win.savedPath = Quickshell.env("HOME") + "/Pictures/Screenshots/screenshot_" + ts + ".png";
|
|
|
|
console.info("screencap", "saving to: " + win.savedPath);
|
|
if (result.saveToFile(win.savedPath)) {
|
|
console.info("screencap", "saved, copying");
|
|
Quickshell.execDetached({
|
|
command: ["sh", "-c", "cat '" + win.savedPath + "' | wl-copy --type image/png"]
|
|
});
|
|
win.savedSuccess = true;
|
|
} else {
|
|
console.info("screencap", "save failed");
|
|
win.savedSuccess = false;
|
|
}
|
|
win.processing = false;
|
|
console.info("screencap", "closing window");
|
|
win.close();
|
|
});
|
|
}
|
|
|
|
Component {
|
|
id: ffmpegProc
|
|
Process {
|
|
property string outputPath
|
|
property bool success: false
|
|
|
|
onExited: (code) => {
|
|
console.info("screencap", "ffmpeg exited: " + code);
|
|
success = code === 0;
|
|
|
|
if (success) {
|
|
console.info("screencap", "copying to clipboard");
|
|
Quickshell.execDetached({
|
|
command: ["sh", "-c", "cat '" + outputPath + "' | wl-copy --type image/png"]
|
|
});
|
|
}
|
|
|
|
Quickshell.execDetached({ command: ["rm", root.tempScreenshot] });
|
|
|
|
win.savedSuccess = success;
|
|
win.processing = false;
|
|
console.info("screencap", "done, closing");
|
|
win.close();
|
|
destroy();
|
|
}
|
|
}
|
|
}
|
|
|
|
function saveRegion(rect, suffix) {
|
|
console.info("screencap", "saveRegion started: " + rect.x + "," + rect.y + " " + rect.width + "x" + rect.height);
|
|
screencopy.grabToImage(function(result) {
|
|
console.info("screencap", "full screenshot grabbed for cropping");
|
|
if (!result.saveToFile(root.tempScreenshot)) {
|
|
console.info("screencap", "ERROR: failed to save temp screenshot");
|
|
win.savedSuccess = false;
|
|
win.processing = false;
|
|
win.close();
|
|
return;
|
|
}
|
|
|
|
console.info("screencap", "temp saved, cropping with ffmpeg");
|
|
var ts = Qt.formatDateTime(new Date(), "yyyy-MM-dd_hh-mm-ss");
|
|
win.savedPath = Quickshell.env("HOME") + "/Pictures/Screenshots/screenshot_" + ts + suffix + ".png";
|
|
|
|
ffmpegProc.createObject(win, {
|
|
command: ["ffmpeg", "-i", root.tempScreenshot, "-vf", "crop=" + Math.floor(rect.width) + ":" + Math.floor(rect.height) + ":" + Math.floor(rect.x) + ":" + Math.floor(rect.y), "-y", win.savedPath],
|
|
outputPath: win.savedPath,
|
|
running: true
|
|
});
|
|
});
|
|
}
|
|
|
|
function captureFullscreen() {
|
|
win.processing = true;
|
|
saveFullscreen();
|
|
}
|
|
|
|
function captureWindow(rect) {
|
|
win.processing = true;
|
|
saveRegion(rect, "_window");
|
|
}
|
|
|
|
function captureRegion() {
|
|
if (!ready || !selection.hasSelection) return;
|
|
win.processing = true;
|
|
saveRegion(root.selectedRegion, "_region");
|
|
}
|
|
|
|
ScreencopyView {
|
|
id: screencopy
|
|
anchors.fill: parent
|
|
captureSource: win.screen
|
|
z: -999
|
|
live: false
|
|
|
|
onHasContentChanged: {
|
|
console.info("screencap", "hasContent: " + hasContent);
|
|
if (hasContent) {
|
|
console.info("screencap", "grabbing for preview");
|
|
grabToImage(function(result) {
|
|
console.info("screencap", "preview grabbed: " + result.url);
|
|
frozen.source = result.url;
|
|
readyTimer.start();
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
id: readyTimer
|
|
interval: Metrics.chronoDuration("normal") + 50
|
|
onTriggered: {
|
|
console.info("screencap", "UI ready");
|
|
win.ready = true;
|
|
}
|
|
}
|
|
|
|
Item {
|
|
anchors.fill: parent
|
|
focus: true
|
|
|
|
Keys.onEscapePressed: win.close()
|
|
Keys.onPressed: event => {
|
|
if (event.key === Qt.Key_F) {
|
|
win.captureFullscreen();
|
|
event.accepted = true;
|
|
} else if (event.key === Qt.Key_W) {
|
|
win.windowMode = !win.windowMode;
|
|
event.accepted = true;
|
|
}
|
|
}
|
|
|
|
Image {
|
|
id: bg
|
|
anchors.fill: parent
|
|
source: Config.runtime.appearance.background.path
|
|
fillMode: Image.PreserveAspectCrop
|
|
opacity: 0
|
|
scale: 1
|
|
|
|
layer.enabled: true
|
|
layer.effect: MultiEffect {
|
|
blurEnabled: true
|
|
blur: 1.0
|
|
blurMax: 64
|
|
brightness: -0.1
|
|
}
|
|
|
|
onStatusChanged: {
|
|
if (status === Image.Ready) fadeIn.start();
|
|
}
|
|
|
|
NumberAnimation on opacity {
|
|
id: fadeIn
|
|
to: 1
|
|
duration: Metrics.chronoDuration("normal")
|
|
easing.type: Appearance.animation.easing
|
|
}
|
|
}
|
|
|
|
Item {
|
|
id: container
|
|
anchors.centerIn: parent
|
|
width: win.width
|
|
height: win.height
|
|
|
|
layer.enabled: true
|
|
layer.effect: MultiEffect {
|
|
shadowEnabled: true
|
|
shadowOpacity: 1
|
|
shadowColor: Appearance.m3colors.m3shadow
|
|
}
|
|
|
|
Image {
|
|
id: frozen
|
|
anchors.fill: parent
|
|
fillMode: Image.PreserveAspectFit
|
|
smooth: true
|
|
cache: false
|
|
}
|
|
|
|
Item {
|
|
id: darkOverlay
|
|
anchors.fill: parent
|
|
visible: (selection.hasSelection || selection.selecting) && !win.windowMode
|
|
|
|
Rectangle {
|
|
y: 0
|
|
width: parent.width
|
|
height: selection.sy
|
|
color: "black"
|
|
opacity: 0.5
|
|
}
|
|
Rectangle {
|
|
y: selection.sy + selection.h
|
|
width: parent.width
|
|
height: parent.height - (selection.sy + selection.h)
|
|
color: "black"
|
|
opacity: 0.5
|
|
}
|
|
Rectangle {
|
|
x: 0
|
|
y: selection.sy
|
|
width: selection.sx
|
|
height: selection.h
|
|
color: "black"
|
|
opacity: 0.5
|
|
}
|
|
Rectangle {
|
|
x: selection.sx + selection.w
|
|
y: selection.sy
|
|
width: parent.width - (selection.sx + selection.w)
|
|
height: selection.h
|
|
color: "black"
|
|
opacity: 0.5
|
|
}
|
|
|
|
Rectangle {
|
|
x: selection.sx
|
|
y: selection.sy
|
|
width: selection.w
|
|
height: selection.h
|
|
color: "black"
|
|
opacity: win.processing ? 0.6 : 0
|
|
|
|
Behavior on opacity {
|
|
NumberAnimation { duration: 200 }
|
|
}
|
|
|
|
LoadingIcon {
|
|
anchors.centerIn: parent
|
|
visible: win.processing
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
id: outline
|
|
x: selection.sx
|
|
y: selection.sy
|
|
width: selection.w
|
|
height: selection.h
|
|
color: "transparent"
|
|
border.color: Appearance.m3colors.m3primary
|
|
border.width: 2
|
|
visible: (selection.selecting || selection.hasSelection) && !win.windowMode
|
|
}
|
|
|
|
Rectangle {
|
|
visible: selection.selecting
|
|
anchors.top: outline.bottom
|
|
anchors.topMargin: Metrics.margin(10)
|
|
anchors.horizontalCenter: outline.horizontalCenter
|
|
width: coords.width + 10
|
|
height: coords.height + 10
|
|
color: Appearance.m3colors.m3surface
|
|
radius: Metrics.radius(20)
|
|
|
|
StyledText {
|
|
id: coords
|
|
anchors.centerIn: parent
|
|
font.pixelSize: Metrics.fontSize(14)
|
|
animate: false
|
|
color: Appearance.m3colors.m3onSurface
|
|
property real scaleX: container.width / win.width
|
|
property real scaleY: container.height / win.height
|
|
text: Math.floor(selection.sx/scaleX) + "," + Math.floor(selection.sy/scaleY) + " " + Math.floor(selection.w/scaleX) + "x" + Math.floor(selection.h/scaleY)
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
id: selection
|
|
anchors.fill: parent
|
|
enabled: win.ready && !win.windowMode
|
|
|
|
property real x1: 0
|
|
property real y1: 0
|
|
property real x2: 0
|
|
property real y2: 0
|
|
property bool selecting: false
|
|
property bool hasSelection: false
|
|
|
|
property real xp: 0
|
|
property real yp: 0
|
|
property real wp: 0
|
|
property real hp: 0
|
|
|
|
property real sx: xp * parent.width
|
|
property real sy: yp * parent.height
|
|
property real w: wp * parent.width
|
|
property real h: hp * parent.height
|
|
|
|
onPressed: mouse => {
|
|
if (!win.ready) return;
|
|
x1 = Math.max(0, Math.min(mouse.x, width));
|
|
y1 = Math.max(0, Math.min(mouse.y, height));
|
|
x2 = x1;
|
|
y2 = y1;
|
|
selecting = true;
|
|
hasSelection = false;
|
|
}
|
|
|
|
onPositionChanged: mouse => {
|
|
if (selecting) {
|
|
x2 = Math.max(0, Math.min(mouse.x, width));
|
|
y2 = Math.max(0, Math.min(mouse.y, height));
|
|
xp = Math.min(x1, x2) / width;
|
|
yp = Math.min(y1, y2) / height;
|
|
wp = Math.abs(x2 - x1) / width;
|
|
hp = Math.abs(y2 - y1) / height;
|
|
}
|
|
}
|
|
|
|
onReleased: mouse => {
|
|
if (!selecting) return;
|
|
|
|
x2 = Math.max(0, Math.min(mouse.x, width));
|
|
y2 = Math.max(0, Math.min(mouse.y, height));
|
|
selecting = false;
|
|
|
|
hasSelection = Math.abs(x2 - x1) > 5 && Math.abs(y2 - y1) > 5;
|
|
|
|
if (hasSelection) {
|
|
xp = Math.min(x1, x2) / width;
|
|
yp = Math.min(y1, y2) / height;
|
|
wp = Math.abs(x2 - x1) / width;
|
|
hp = Math.abs(y2 - y1) / height;
|
|
|
|
root.selectedRegion = Qt.rect(
|
|
Math.min(x1, x2) * win.screen.width / width,
|
|
Math.min(y1, y2) * win.screen.height / height,
|
|
Math.abs(x2 - x1) * win.screen.width / width,
|
|
Math.abs(y2 - y1) * win.screen.height / height
|
|
);
|
|
|
|
win.captureRegion();
|
|
} else {
|
|
win.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
Repeater {
|
|
model: {
|
|
if (!win.windowMode || !win.ready) return [];
|
|
var ws = Hyprland.focusedMonitor?.activeWorkspace;
|
|
return ws?.toplevels ? ws.toplevels.values : [];
|
|
}
|
|
|
|
delegate: Item {
|
|
required property var modelData
|
|
property var w: modelData?.lastIpcObject
|
|
visible: w?.at && w?.size
|
|
|
|
property real barX: 0
|
|
property real barY: 0
|
|
property real sx: container.width / (win.screen.width - barX)
|
|
property real sy: container.height / (win.screen.height - barY)
|
|
|
|
x: visible ? (w.at[0] - barX) * sx : 0
|
|
y: visible ? (w.at[1] - barY) * sy : 0
|
|
width: visible ? w.size[0] * sx : 0
|
|
height: visible ? w.size[1] * sy : 0
|
|
z: w?.floating ? (hover.containsMouse ? 1000 : 100) : (hover.containsMouse ? 50 : 0)
|
|
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
color: "transparent"
|
|
border.color: Appearance.m3colors.m3primary
|
|
border.width: hover.containsMouse ? 3 : 0
|
|
radius: Metrics.radius(8)
|
|
Behavior on border.width {
|
|
NumberAnimation { duration: Metrics.chronoDuration(150) }
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
color: Appearance.m3colors.m3primary
|
|
opacity: hover.containsMouse ? 0.15 : 0
|
|
radius: Metrics.radius(8)
|
|
Behavior on opacity {
|
|
NumberAnimation { duration: Metrics.chronoDuration(150) }
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
id: hover
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: {
|
|
win.captureWindow(Qt.rect(w.at[0], w.at[1], w.size[0], w.size[1]));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ParallelAnimation {
|
|
running: win.visible && !win.closing && frozen.source != ""
|
|
|
|
NumberAnimation {
|
|
target: bg
|
|
property: "scale"
|
|
to: bg.scale + 0.05
|
|
duration: Metrics.chronoDuration("normal")
|
|
easing.type: Appearance.animation.easing
|
|
}
|
|
NumberAnimation {
|
|
target: container
|
|
property: "width"
|
|
to: win.width * 0.8
|
|
duration: Metrics.chronoDuration("normal")
|
|
easing.type: Appearance.animation.easing
|
|
}
|
|
NumberAnimation {
|
|
target: container
|
|
property: "height"
|
|
to: win.height * 0.8
|
|
duration: Metrics.chronoDuration("normal")
|
|
easing.type: Appearance.animation.easing
|
|
}
|
|
}
|
|
|
|
ParallelAnimation {
|
|
id: closeAnim
|
|
|
|
NumberAnimation {
|
|
target: bg
|
|
property: "scale"
|
|
to: bg.scale - 0.05
|
|
duration: Metrics.chronoDuration("normal")
|
|
easing.type: Appearance.animation.easing
|
|
}
|
|
NumberAnimation {
|
|
target: container
|
|
property: "width"
|
|
to: win.width
|
|
duration: Metrics.chronoDuration("normal")
|
|
easing.type: Appearance.animation.easing
|
|
}
|
|
NumberAnimation {
|
|
target: container
|
|
property: "height"
|
|
to: win.height
|
|
duration: Metrics.chronoDuration("normal")
|
|
easing.type: Appearance.animation.easing
|
|
}
|
|
NumberAnimation {
|
|
target: darkOverlay
|
|
property: "opacity"
|
|
to: 0
|
|
duration: Metrics.chronoDuration("normal")
|
|
easing.type: Appearance.animation.easing
|
|
}
|
|
NumberAnimation {
|
|
target: outline
|
|
property: "opacity"
|
|
to: 0
|
|
duration: Metrics.chronoDuration("normal")
|
|
easing.type: Appearance.animation.easing
|
|
}
|
|
|
|
onFinished: {
|
|
root.active = false;
|
|
if (win.savedSuccess) {
|
|
Quickshell.execDetached({
|
|
command: ["notify-send", "Screenshot saved", win.savedPath.split("/").pop() + " (copied)"]
|
|
});
|
|
} else if (win.savedPath !== "") {
|
|
Quickshell.execDetached({
|
|
command: ["notify-send", "Screenshot failed", "Could not save"]
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Item {
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
anchors.bottom: parent.bottom
|
|
anchors.bottomMargin: Metrics.margin(30)
|
|
width: row.width + 20
|
|
height: row.height + 20
|
|
visible: true
|
|
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
color: Appearance.m3colors.m3surface
|
|
radius: Metrics.radius("large")
|
|
}
|
|
|
|
RowLayout {
|
|
id: row
|
|
anchors.centerIn: parent
|
|
|
|
StyledButton {
|
|
icon: "fullscreen"
|
|
text: "Full screen"
|
|
tooltipText: "Capture the whole screen [F]"
|
|
onClicked: win.captureFullscreen()
|
|
}
|
|
Rectangle {
|
|
Layout.fillHeight: true
|
|
width: 2
|
|
color: Appearance.m3colors.m3onSurfaceVariant
|
|
opacity: 0.2
|
|
}
|
|
StyledButton {
|
|
icon: "window"
|
|
checkable: true
|
|
checked: win.windowMode
|
|
text: "Window"
|
|
tooltipText: "Hover and click a window [W]"
|
|
onClicked: win.windowMode = !win.windowMode
|
|
}
|
|
StyledButton {
|
|
secondary: true
|
|
icon: "close"
|
|
tooltipText: "Exit [Escape]"
|
|
onClicked: win.close()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
HyprlandFocusGrab {
|
|
id: grab
|
|
windows: [win]
|
|
}
|
|
|
|
onVisibleChanged: {
|
|
if (visible) grab.active = true
|
|
}
|
|
|
|
Connections {
|
|
target: grab
|
|
function onActiveChanged() {
|
|
if (!grab.active && !win.closing) win.close();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|