From 5a0f7f3ef33e66364312bdaca80810ce409df35d Mon Sep 17 00:00:00 2001 From: Uyanide Date: Mon, 9 Mar 2026 22:58:23 +0100 Subject: [PATCH] qs: add replay functionality (yet disabled) --- config/niri/.config/niri/config/binds.kdl | 1 + config/niri/.config/niri/config/envs.kdl | 2 +- config/niri/.config/niri/config/misc.kdl | 2 +- config/niri/.config/niri/config/prime.kdl | 6 +- .../quickshell/Services/IPCService.qml | 4 + .../quickshell/Services/RecordService.qml | 151 +++++++++++++++--- config/scripts/.local/scripts/quickshell-kill | 13 +- config/scripts/.local/snippets/set_display | 2 +- 8 files changed, 152 insertions(+), 29 deletions(-) diff --git a/config/niri/.config/niri/config/binds.kdl b/config/niri/.config/niri/config/binds.kdl index b558ce7..4c12ed6 100644 --- a/config/niri/.config/niri/config/binds.kdl +++ b/config/niri/.config/niri/config/binds.kdl @@ -29,6 +29,7 @@ binds { Mod+Shift+K { spawn-sh "quickshell-kill || quickshell"; } Mod+I { spawn "qs" "ipc" "call" "idleInhibitor" "toggle"; } Mod+Alt+R { spawn "qs" "ipc" "call" "recording" "startOrStop"; } + Mod+Alt+G { spawn "qs" "ipc" "call" "recording" "saveReplay"; } Mod+Shift+E { spawn "qs" "ipc" "call" "sunset" "toggle"; } Mod+X { spawn "qs" "ipc" "call" "notes" "openRecent"; } Mod+Shift+X { spawn "qs" "ipc" "call" "notes" "create"; } diff --git a/config/niri/.config/niri/config/envs.kdl b/config/niri/.config/niri/config/envs.kdl index a5832c3..8c88c31 100644 --- a/config/niri/.config/niri/config/envs.kdl +++ b/config/niri/.config/niri/config/envs.kdl @@ -24,6 +24,6 @@ environment { XCURSOR_THEME "Bibata-Modern-Ice" XCURSOR_SIZE "24" ELECTRON_OZONE_PLATFORM_HINT "wayland" - QSG_RHI_BACKEND "vulkan" + // QSG_RHI_BACKEND "vulkan" // GSK_RENDERER "vulkan" } diff --git a/config/niri/.config/niri/config/misc.kdl b/config/niri/.config/niri/config/misc.kdl index db5af45..8c18b91 100644 --- a/config/niri/.config/niri/config/misc.kdl +++ b/config/niri/.config/niri/config/misc.kdl @@ -1,7 +1,7 @@ screenshot-path "~/Pictures/Screenshots/niri_screenshot_%Y-%m-%d_%H-%M-%S.png" debug { - render-drm-device "/dev/dri/renderD128" + render-drm-device "/dev/dri/renderD129" } // gestures { diff --git a/config/niri/.config/niri/config/prime.kdl b/config/niri/.config/niri/config/prime.kdl index 8b13789..d6062de 100644 --- a/config/niri/.config/niri/config/prime.kdl +++ b/config/niri/.config/niri/config/prime.kdl @@ -1 +1,5 @@ - +environment { + __NV_PRIME_RENDER_OFFLOAD "1" + __VK_LAYER_NV_optimus "NVIDIA_only" + __GLX_VENDOR_LIBRARY_NAME "nvidia" +} diff --git a/config/quickshell/.config/quickshell/Services/IPCService.qml b/config/quickshell/.config/quickshell/Services/IPCService.qml index 0178f1d..e3a8cff 100644 --- a/config/quickshell/.config/quickshell/Services/IPCService.qml +++ b/config/quickshell/.config/quickshell/Services/IPCService.qml @@ -10,6 +10,10 @@ Item { RecordService.startOrStop(); } + function saveReplay() { + RecordService.stopReplay(); + } + target: "recording" } diff --git a/config/quickshell/.config/quickshell/Services/RecordService.qml b/config/quickshell/.config/quickshell/Services/RecordService.qml index d54d09f..e63daff 100644 --- a/config/quickshell/.config/quickshell/Services/RecordService.qml +++ b/config/quickshell/.config/quickshell/Services/RecordService.qml @@ -7,18 +7,33 @@ import qs.Utils pragma Singleton Singleton { + // Connections { + // function onSinkChanged() { + // if (!isReplayInitStarted && AudioService.sink) { + // Logger.i("RecordService", "Audio sink available, starting replay buffer."); + // startReplay(); + // isReplayInitStarted = true; + // } + // } + // target: AudioService + // } + readonly property string recordingDir: Paths.recordingDir property bool isRecording: false + property bool isReplayInitStarted: false + property bool isReplayStarted: false property bool isStopping: false + property bool isReplayStopping: false readonly property string codec: "libx264" readonly property string container: "mkv" readonly property string pixelFormat: "yuv420p" property string recordingDisplay: "" + readonly property int replayDuration: 15 readonly property int framerate: 60 readonly property var codecParams: Object.freeze(["preset=ultrafast", "crf=15", "tune=zerolatency", "color_range=tv"]) readonly property var filterArgs: "" - function getFilename() { + function getFilename(prefix = "recording") { var d = new Date(); var year = d.getFullYear(); var month = ("0" + (d.getMonth() + 1)).slice(-2); @@ -26,7 +41,7 @@ Singleton { var hours = ("0" + d.getHours()).slice(-2); var minutes = ("0" + d.getMinutes()).slice(-2); var seconds = ("0" + d.getSeconds()).slice(-2); - return "recording_" + year + "-" + month + "-" + day + "_" + hours + "." + minutes + "." + seconds + "." + container; + return prefix + "_" + year + "-" + month + "-" + day + "_" + hours + "." + minutes + "." + seconds + "." + container; } function getAudioSink() { @@ -37,6 +52,15 @@ Singleton { return Niri.focusedOutput || null; } + function getPrimaryVideoSource() { + for (const screen of Quickshell.screens) { + if (screen.name === "DP-1") + return screen.name; + + } + return Quickshell.screens.length ? Quickshell.screens[0].name : null; + } + function startOrStop() { if (isRecording) stop(); @@ -76,7 +100,7 @@ Singleton { Logger.e("RecordService", "No audio sink available."); return ; } - recordProcess.filePath = recordingDir + getFilename(); + recordProcess.filePath = recordingDir + getFilename("recording"); recordProcess.command = ["wf-recorder", "--audio=" + audioSink, "-o", source, "--codec", codec, "--pixel-format", pixelFormat, "--framerate", framerate.toString(), "-f", recordProcess.filePath]; for (const param of codecParams) { recordProcess.command.push("-p"); @@ -87,39 +111,78 @@ Singleton { recordProcess.command.push(filterArgs); } Logger.i("RecordService", "Starting recording with command: " + recordProcess.command.join(" ")); - recordProcess.onErrorExit = function() { - Logger.e("RecordService", "Recording process exited with an error."); - SendNotification.show("Recording failed", "An error occurred while trying to record the screen."); - }; - recordProcess.onNormalExit = function() { - Logger.i("RecordService", "Recording stopped, file saved to: " + recordProcess.filePath); - SendNotification.show("Recording stopped", recordProcess.filePath); - }; recordProcess.running = true; SendNotification.show("Recording started", "Recording to " + recordProcess.filePath); } + function startReplay() { + if (isReplayStarted || isReplayStopping) { + Logger.w("RecordService", "Replay buffer already active, cannot start."); + return ; + } + isReplayStarted = true; + const source = getPrimaryVideoSource(); + if (!source) { + SendNotification.show("Replay buffer failed", "Could not determine which display to record from."); + Logger.e("RecordService", "No recording source available for replay buffer."); + return ; + } + const audioSink = getAudioSink(); + if (!audioSink) { + SendNotification.show("Replay buffer failed", "No audio sink available to record from."); + Logger.e("RecordService", "No audio sink available for replay buffer."); + return ; + } + replayProcess.filePath = recordingDir + getFilename("replay"); + replayProcess.command = ["wf-recorder", "--history", replayDuration, "--audio=" + audioSink, "-o", source, "--codec", codec, "--pixel-format", pixelFormat, "--framerate", framerate.toString(), "-f", replayProcess.filePath]; + for (const param of codecParams) { + replayProcess.command.push("-p"); + replayProcess.command.push(param); + } + if (filterArgs !== "") { + replayProcess.command.push("-F"); + replayProcess.command.push(filterArgs); + } + Logger.i("RecordService", "Starting replay buffer with command: " + replayProcess.command.join(" ")); + replayProcess.running = true; + } + + function stopReplay() { + if (!isReplayStarted) { + Logger.w("RecordService", "Replay buffer not active, cannot stop."); + return ; + } + if (isReplayStopping) { + Logger.w("RecordService", "Already stopping replay buffer, please wait."); + return ; + } + isReplayStopping = true; + replayStopTimeout.restart(); + replayProcess.signal(10); // SIGUSR1 + } + + Component.onDestruction: function() { + if (recordProcess.running) + recordProcess.running = false; + + if (replayProcess.running) + replayProcess.signal(2); + + } + Process { id: recordProcess property string filePath: "" - property var onNormalExit: null - property var onErrorExit: null running: false onExited: function(exitCode, exitStatus) { if (exitCode === 0) { Logger.i("RecordService", "Recording stopped successfully."); - if (onNormalExit) { - onNormalExit(); - onNormalExit = null; - } + SendNotification.show("Recording stopped", "File saved to: " + filePath); } else { Logger.e("RecordService", "Recording process exited with error code: " + exitCode); - if (onErrorExit) { - onErrorExit(); - onErrorExit = null; - } + SendNotification.show("Recording failed", "An error occurred while trying to record the screen."); } isRecording = false; isStopping = false; @@ -127,4 +190,50 @@ Singleton { } } + Process { + id: replayProcess + + property string filePath: "" + + running: false + onExited: function(exitCode, exitStatus) { + replayStopTimeout.stop(); + if (exitCode === 0) { + Logger.i("RecordService", "Replay buffer saved successfully."); + SendNotification.show("Replay saved", "File saved to: " + filePath); + } else { + Logger.e("RecordService", "Replay process exited with error code: " + exitCode); + SendNotification.show("Replay failed", "An error occurred while trying to record the replay buffer."); + } + isReplayStarted = false; + isReplayStopping = false; + replayRestartTimer.restart(); + } + } + + Timer { + id: replayRestartTimer + + interval: 1000 + repeat: false + onTriggered: function() { + if (!isReplayStarted && !isReplayStopping) + startReplay(); + + } + } + + Timer { + id: replayStopTimeout + + interval: 5000 + repeat: false + onTriggered: function() { + if (isReplayStopping) { + Logger.w("RecordService", "Replay buffer did not stop in time, killing process."); + replayProcess.running = false; + } + } + } + } diff --git a/config/scripts/.local/scripts/quickshell-kill b/config/scripts/.local/scripts/quickshell-kill index 953966e..6f72a19 100755 --- a/config/scripts/.local/scripts/quickshell-kill +++ b/config/scripts/.local/scripts/quickshell-kill @@ -3,11 +3,16 @@ pid=$(pgrep -x quickshell) [ -z "$pid" ] && exit 1 -for child in $(pgrep -P "$pid" 2>/dev/null); do - kill "$child" -done +# for child in $(pgrep -P "$pid" 2>/dev/null); do +# kill "$child" +# done -sleep 0.3 +children=$(pgrep -P "$pid" 2>/dev/null) kill "$pid" +sleep 0.5 + +for child in $children; do + kill "$child" || true +done diff --git a/config/scripts/.local/snippets/set_display b/config/scripts/.local/snippets/set_display index 9a2ea66..a0f46de 100644 --- a/config/scripts/.local/snippets/set_display +++ b/config/scripts/.local/snippets/set_display @@ -9,7 +9,7 @@ # Constants niri_config_file="$HOME/.config/niri/config/misc.kdl" -prefer_order=(intel nvidia) +prefer_order=(nvidia intel) # Get vendor and path of each GPU default_card_path="$(find /dev/dri/card* 2>/dev/null | head -n 1)"