quickshell: run slurp from the recorder card, not inside the script

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-08-27 10:19:07 +01:00
parent 87217f4675
commit 0d0009ca8e

View file

@ -106,12 +106,21 @@ in
pw=$(${pkgs.zenity}/bin/zenity --password --title="WiFi Password" 2>/dev/null) pw=$(${pkgs.zenity}/bin/zenity --password --title="WiFi Password" 2>/dev/null)
[ -n "$pw" ] && ${pkgs.networkmanager}/bin/nmcli device wifi connect "$ssid" password "$pw" [ -n "$pw" ] && ${pkgs.networkmanager}/bin/nmcli device wifi connect "$ssid" password "$pw"
''; '';
# Screen recording, driven by the quick-settings card. The shell execs # Screen recording, driven by the quick-settings card. $1 is a slurp
# into wf-recorder so the card's SIGINT lands on the recorder itself — # geometry ("" = whole output), $2 is "audio" or anything else.
# wf-recorder finalises the mp4 on SIGINT, killing a wrapper instead #
# would leave a truncated file. The path is echoed before the exec so # The region pick deliberately does NOT happen here. A wrapper parked
# the card can offer the result. Audio means the default sink's monitor # in $(slurp) is a process the card cannot stop: non-interactive bash
# (desktop sound), not the microphone. # defers a signal until its foreground child exits, so Stop did
# nothing, and the wrapper's lifetime made the card claim it was
# recording while slurp was still waiting for a drag. The card runs
# slurp itself and passes the result in.
#
# Everything before the exec is a few ms of setup, so the process the
# card signals is wf-recorder itself — which finalises the mp4 on
# SIGINT, where killing a wrapper would leave a truncated file. The
# path is echoed first so the card can offer the result. Audio means
# the default sink's monitor (desktop sound), not the microphone.
screenRecScript = pkgs.writeShellScript "screen-rec" '' screenRecScript = pkgs.writeShellScript "screen-rec" ''
out="$HOME/Videos/rec-$(${pkgs.coreutils}/bin/date +%Y%m%d-%H%M%S).mp4" out="$HOME/Videos/rec-$(${pkgs.coreutils}/bin/date +%Y%m%d-%H%M%S).mp4"
${pkgs.coreutils}/bin/mkdir -p "$HOME/Videos" ${pkgs.coreutils}/bin/mkdir -p "$HOME/Videos"
@ -121,12 +130,10 @@ in
| ${pkgs.gnused}/bin/sed -n 's/.*node\.name = "\(.*\)".*/\1/p') | ${pkgs.gnused}/bin/sed -n 's/.*node\.name = "\(.*\)".*/\1/p')
[ -n "$sink" ] && audio="--audio=$sink.monitor" [ -n "$sink" ] && audio="--audio=$sink.monitor"
fi fi
if [ "$1" = region ]; then
geom=$(${pkgs.slurp}/bin/slurp) || exit 1
echo "$out" echo "$out"
exec ${pkgs.wf-recorder}/bin/wf-recorder $audio -g "$geom" -f "$out" if [ -n "$1" ]; then
exec ${pkgs.wf-recorder}/bin/wf-recorder $audio -g "$1" -f "$out"
fi fi
echo "$out"
exec ${pkgs.wf-recorder}/bin/wf-recorder $audio -f "$out" exec ${pkgs.wf-recorder}/bin/wf-recorder $audio -f "$out"
''; '';
nmcli = "${pkgs.networkmanager}/bin/nmcli"; nmcli = "${pkgs.networkmanager}/bin/nmcli";
@ -1417,6 +1424,7 @@ in
readonly property string wxFetch: "${wxFetchScript}" readonly property string wxFetch: "${wxFetchScript}"
readonly property string xdgOpen: "${pkgs.xdg-utils}/bin/xdg-open" readonly property string xdgOpen: "${pkgs.xdg-utils}/bin/xdg-open"
readonly property string screenRec: "${screenRecScript}" readonly property string screenRec: "${screenRecScript}"
readonly property string slurp: "${pkgs.slurp}/bin/slurp"
readonly property string themeApply: "${themeApply}" readonly property string themeApply: "${themeApply}"
} }
''; '';
@ -6259,19 +6267,53 @@ in
Card { Card {
id: recCard id: recCard
width: parent.width width: parent.width
// recProc is wf-recorder itself, so `recording` is
// true only while frames are actually being
// written the region pick is its own state.
readonly property bool recording: recProc.running readonly property bool recording: recProc.running
readonly property bool selecting: slurpProc.running
property bool audio: false property bool audio: false
property int elapsed: 0 property int elapsed: 0
property string lastFile: "" property string lastFile: ""
function start(mode) { function start(mode) {
if (recording || selecting) return;
lastFile = ""; lastFile = "";
elapsed = 0; // slurp wants the screen to itself, and the
recProc.command = [Commands.screenRec, mode, recCard.audio ? "audio" : "silent"]; // panel would otherwise be in frame.
recProc.running = true;
// The panel would otherwise be in the shot
// and slurp needs the screen to itself.
qsDropdown.animateClose(); qsDropdown.animateClose();
if (mode === "region") slurpProc.running = true;
else launch("");
}
function launch(geom) {
elapsed = 0;
recProc.command = [Commands.screenRec, geom, recCard.audio ? "audio" : "silent"];
recProc.running = true;
}
// Region pick lives here, not in the script: bash
// blocked in $(slurp) swallows the Stop signal and
// makes the card claim it is recording while slurp
// is still waiting for a drag.
Process {
id: slurpProc
command: [Commands.slurp]
property string geom: ""
// SplitParser, not StdioCollector: the line has
// to be in hand by the time exited fires.
stdout: SplitParser { onRead: data => slurpProc.geom = data.trim() }
// Cancelled with Escape or right-click: slurp
// prints nothing, so an empty geom is the
// cancel signal and nothing starts. (Watching
// running, not exited: exited's exitStatus
// parameter is a C++ enum qmllint can't
// resolve, and the exit code adds nothing.)
onRunningChanged: {
if (running) return;
if (geom !== "") recCard.launch(geom);
geom = "";
}
} }
Process { Process {
@ -6300,13 +6342,15 @@ in
SIcon { SIcon {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: "video" text: "video"
color: recCard.recording ? Theme.err : Theme.base05 color: recCard.recording ? Theme.err
: recCard.selecting ? Theme.base0D : Theme.base05
font.pixelSize: 15 font.pixelSize: 15
} }
SText { SText {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: recCard.recording text: recCard.recording
? Math.floor(recCard.elapsed / 60) + ":" + ("0" + (recCard.elapsed % 60)).slice(-2) ? Math.floor(recCard.elapsed / 60) + ":" + ("0" + (recCard.elapsed % 60)).slice(-2)
: recCard.selecting ? "Drag a region"
: "Screen Recording" : "Screen Recording"
font.weight: Font.Medium font.weight: Font.Medium
} }
@ -6316,7 +6360,7 @@ in
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: 6 spacing: 6
visible: !recCard.recording visible: !recCard.recording && !recCard.selecting
SText { SText {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: "Audio" text: "Audio"
@ -6334,7 +6378,7 @@ in
Row { Row {
width: parent.width width: parent.width
spacing: 6 spacing: 6
visible: !recCard.recording visible: !recCard.recording && !recCard.selecting
Repeater { Repeater {
model: [ model: [
@ -6369,26 +6413,35 @@ in
} }
} }
// Doubles as the way out of a region pick: if slurp
// ever comes up without input (its grab can be
// dropped the same way the bar's is), Escape never
// reaches it and this is the only escape hatch.
Rectangle { Rectangle {
visible: recCard.recording visible: recCard.recording || recCard.selecting
width: parent.width width: parent.width
height: 26 height: 26
radius: Theme.radiusSmall radius: Theme.radiusSmall
color: Theme.err color: recCard.recording ? Theme.err : Theme.base02t
border.color: Theme.base03
border.width: recCard.recording ? 0 : 1
SText { SText {
anchors.centerIn: parent anchors.centerIn: parent
text: "Stop" text: recCard.recording ? "Stop" : "Cancel"
font.pixelSize: 11 font.pixelSize: 11
color: Theme.base00 color: recCard.recording ? Theme.base00 : Theme.base05
} }
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
// SIGINT, not kill wf-recorder writes the onClicked: {
// container trailer on it. // SIGINT, not kill wf-recorder writes
onClicked: recProc.signal(2) // the container trailer on it.
if (recCard.recording) recProc.signal(2);
else slurpProc.running = false;
}
} }
} }