mirror of
https://github.com/belsabbagh/dotfiles.git
synced 2026-04-11 09:36:46 +00:00
quickshell and hyprland additions
This commit is contained in:
@@ -0,0 +1,606 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user