| 1 | pragma Singleton |
| 2 | import QtQuick |
| 3 | import Quickshell |
| 4 | import Quickshell.Io |
| 5 | import Quickshell.Hyprland |
| 6 | import Quickshell.Services.Pipewire |
| 7 | |
| 8 | // Screen recording with wf-recorder (the focused monitor, or a region via slurp), saved to ~/vids/rec. |
| 9 | // Records desktop audio: the monitor source of the current default output (whatever you hear). |
| 10 | // Two steps so each can be cancelled: `selecting` (slurp is up) then `running` (wf-recorder records). |
| 11 | // stop() works in both. Everything is hidden if wf-recorder / slurp aren't installed. |
| 12 | Singleton { |
| 13 | id: root |
| 14 | property bool available: false |
| 15 | readonly property bool running: recProc.running |
| 16 | readonly property bool selecting: delay.running || selectProc.running |
| 17 | readonly property bool busy: running || selecting |
| 18 | property string dir: Quickshell.env("HOME") + "/vids/recs" |
| 19 | property string file: "" |
| 20 | readonly property string log: Quickshell.env("HOME") + "/.cache/aurora/rec.log" |
| 21 | property string mode: "screen" |
| 22 | property int elapsed: 0 // seconds |
| 23 | |
| 24 | function start(m) { // "region" | "screen" |
| 25 | if (!available || busy) return |
| 26 | mode = m |
| 27 | delay.start() // let the panel close before slurp grabs the screen |
| 28 | } |
| 29 | function stop() { |
| 30 | if (delay.running) delay.stop() |
| 31 | if (selectProc.running) selectProc.signal(15) |
| 32 | if (recProc.running) recProc.signal(2) // SIGINT: wf-recorder finalises the file |
| 33 | } |
| 34 | function toggle(m) { if (busy) stop(); else start(m) } |
| 35 | |
| 36 | function record(geometry) { |
| 37 | file = dir + "/" + Qt.formatDateTime(new Date(), "yyyy-MM-dd_HH-mm-ss") + ".mp4" |
| 38 | var mon = Hyprland.focusedMonitor |
| 39 | // exec so the Process pid is wf-recorder itself and SIGINT reaches it. |
| 40 | // its (very chatty) output must go to a file: left on the Process pipe it dies without writing anything |
| 41 | var sink = Pipewire.defaultAudioSink |
| 42 | var audio = sink ? "--audio=" + sink.name + ".monitor" : "--audio" |
| 43 | var run = 'mkdir -p "${1%/*}" "${3%/*}"; exec wf-recorder ' + (geometry !== "" ? '-g' : '-o') + ' "$2" "$4" -f "$1" >"$3" 2>&1' |
| 44 | recProc.command = ["sh", "-c", run, "sh", file, geometry !== "" ? geometry : (mon ? mon.name : ""), log, audio] |
| 45 | elapsed = 0 |
| 46 | recProc.running = true |
| 47 | } |
| 48 | |
| 49 | Timer { |
| 50 | id: delay |
| 51 | interval: 250 |
| 52 | onTriggered: { |
| 53 | if (root.mode === "region") selectProc.running = true |
| 54 | else root.record("") |
| 55 | } |
| 56 | } |
| 57 | Process { |
| 58 | id: selectProc |
| 59 | command: ["slurp"] |
| 60 | // slurp prints nothing when cancelled (Esc, or stop() killing it) |
| 61 | stdout: StdioCollector { |
| 62 | onStreamFinished: { |
| 63 | var g = text.trim() |
| 64 | if (g !== "") root.record(g) |
| 65 | } |
| 66 | } |
| 67 | } |
| 68 | Process { |
| 69 | id: recProc |
| 70 | onExited: { |
| 71 | Quickshell.execDetached(["sh", "-c", |
| 72 | '[ -s "$1" ] && notify-send -a aurora "recording saved" "$1"', "sh", root.file]) |
| 73 | } |
| 74 | } |
| 75 | Timer { interval: 1000; repeat: true; running: recProc.running; onTriggered: root.elapsed++ } |
| 76 | |
| 77 | Process { |
| 78 | command: ["sh", "-c", "command -v wf-recorder && command -v slurp"] |
| 79 | running: true |
| 80 | onExited: code => root.available = code === 0 |
| 81 | } |
| 82 | } |