From 0d0009ca8e766adb18266f798c75c851dc1bbfc0 Mon Sep 17 00:00:00 2001 From: rope Date: Thu, 27 Aug 2026 10:19:07 +0100 Subject: [PATCH] quickshell: run slurp from the recorder card, not inside the script Co-Authored-By: Claude Opus 5 --- settings/quickshell.nix | 105 ++++++++++++++++++++++++++++++---------- 1 file changed, 79 insertions(+), 26 deletions(-) diff --git a/settings/quickshell.nix b/settings/quickshell.nix index 4207010..f3625be 100644 --- a/settings/quickshell.nix +++ b/settings/quickshell.nix @@ -106,12 +106,21 @@ in pw=$(${pkgs.zenity}/bin/zenity --password --title="WiFi Password" 2>/dev/null) [ -n "$pw" ] && ${pkgs.networkmanager}/bin/nmcli device wifi connect "$ssid" password "$pw" ''; - # Screen recording, driven by the quick-settings card. The shell execs - # into wf-recorder so the card's SIGINT lands on the recorder itself — - # 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 card can offer the result. Audio means the default sink's monitor - # (desktop sound), not the microphone. + # Screen recording, driven by the quick-settings card. $1 is a slurp + # geometry ("" = whole output), $2 is "audio" or anything else. + # + # The region pick deliberately does NOT happen here. A wrapper parked + # in $(slurp) is a process the card cannot stop: non-interactive bash + # 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" '' out="$HOME/Videos/rec-$(${pkgs.coreutils}/bin/date +%Y%m%d-%H%M%S).mp4" ${pkgs.coreutils}/bin/mkdir -p "$HOME/Videos" @@ -121,12 +130,10 @@ in | ${pkgs.gnused}/bin/sed -n 's/.*node\.name = "\(.*\)".*/\1/p') [ -n "$sink" ] && audio="--audio=$sink.monitor" fi - if [ "$1" = region ]; then - geom=$(${pkgs.slurp}/bin/slurp) || exit 1 - echo "$out" - exec ${pkgs.wf-recorder}/bin/wf-recorder $audio -g "$geom" -f "$out" - fi echo "$out" + if [ -n "$1" ]; then + exec ${pkgs.wf-recorder}/bin/wf-recorder $audio -g "$1" -f "$out" + fi exec ${pkgs.wf-recorder}/bin/wf-recorder $audio -f "$out" ''; nmcli = "${pkgs.networkmanager}/bin/nmcli"; @@ -1417,6 +1424,7 @@ in readonly property string wxFetch: "${wxFetchScript}" readonly property string xdgOpen: "${pkgs.xdg-utils}/bin/xdg-open" readonly property string screenRec: "${screenRecScript}" + readonly property string slurp: "${pkgs.slurp}/bin/slurp" readonly property string themeApply: "${themeApply}" } ''; @@ -6259,19 +6267,53 @@ in Card { id: recCard 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 selecting: slurpProc.running property bool audio: false property int elapsed: 0 property string lastFile: "" function start(mode) { + if (recording || selecting) return; lastFile = ""; - elapsed = 0; - recProc.command = [Commands.screenRec, mode, recCard.audio ? "audio" : "silent"]; - recProc.running = true; - // The panel would otherwise be in the shot — - // and slurp needs the screen to itself. + // slurp wants the screen to itself, and the + // panel would otherwise be in frame. 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 { @@ -6300,13 +6342,15 @@ in SIcon { anchors.verticalCenter: parent.verticalCenter text: "video" - color: recCard.recording ? Theme.err : Theme.base05 + color: recCard.recording ? Theme.err + : recCard.selecting ? Theme.base0D : Theme.base05 font.pixelSize: 15 } SText { anchors.verticalCenter: parent.verticalCenter text: recCard.recording ? Math.floor(recCard.elapsed / 60) + ":" + ("0" + (recCard.elapsed % 60)).slice(-2) + : recCard.selecting ? "Drag a region" : "Screen Recording" font.weight: Font.Medium } @@ -6316,7 +6360,7 @@ in anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter spacing: 6 - visible: !recCard.recording + visible: !recCard.recording && !recCard.selecting SText { anchors.verticalCenter: parent.verticalCenter text: "Audio" @@ -6334,7 +6378,7 @@ in Row { width: parent.width spacing: 6 - visible: !recCard.recording + visible: !recCard.recording && !recCard.selecting Repeater { 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 { - visible: recCard.recording + visible: recCard.recording || recCard.selecting width: parent.width height: 26 radius: Theme.radiusSmall - color: Theme.err + color: recCard.recording ? Theme.err : Theme.base02t + border.color: Theme.base03 + border.width: recCard.recording ? 0 : 1 SText { anchors.centerIn: parent - text: "Stop" + text: recCard.recording ? "Stop" : "Cancel" font.pixelSize: 11 - color: Theme.base00 + color: recCard.recording ? Theme.base00 : Theme.base05 } MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor - // SIGINT, not kill — wf-recorder writes the - // container trailer on it. - onClicked: recProc.signal(2) + onClicked: { + // SIGINT, not kill — wf-recorder writes + // the container trailer on it. + if (recCard.recording) recProc.signal(2); + else slurpProc.running = false; + } } }