qs: add replay functionality (yet disabled)

This commit is contained in:
2026-03-09 22:58:23 +01:00
parent 9fc7ab92ff
commit 5a0f7f3ef3
8 changed files with 152 additions and 29 deletions
@@ -29,6 +29,7 @@ binds {
Mod+Shift+K { spawn-sh "quickshell-kill || quickshell"; } Mod+Shift+K { spawn-sh "quickshell-kill || quickshell"; }
Mod+I { spawn "qs" "ipc" "call" "idleInhibitor" "toggle"; } Mod+I { spawn "qs" "ipc" "call" "idleInhibitor" "toggle"; }
Mod+Alt+R { spawn "qs" "ipc" "call" "recording" "startOrStop"; } 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+Shift+E { spawn "qs" "ipc" "call" "sunset" "toggle"; }
Mod+X { spawn "qs" "ipc" "call" "notes" "openRecent"; } Mod+X { spawn "qs" "ipc" "call" "notes" "openRecent"; }
Mod+Shift+X { spawn "qs" "ipc" "call" "notes" "create"; } Mod+Shift+X { spawn "qs" "ipc" "call" "notes" "create"; }
+1 -1
View File
@@ -24,6 +24,6 @@ environment {
XCURSOR_THEME "Bibata-Modern-Ice" XCURSOR_THEME "Bibata-Modern-Ice"
XCURSOR_SIZE "24" XCURSOR_SIZE "24"
ELECTRON_OZONE_PLATFORM_HINT "wayland" ELECTRON_OZONE_PLATFORM_HINT "wayland"
QSG_RHI_BACKEND "vulkan" // QSG_RHI_BACKEND "vulkan"
// GSK_RENDERER "vulkan" // GSK_RENDERER "vulkan"
} }
+1 -1
View File
@@ -1,7 +1,7 @@
screenshot-path "~/Pictures/Screenshots/niri_screenshot_%Y-%m-%d_%H-%M-%S.png" screenshot-path "~/Pictures/Screenshots/niri_screenshot_%Y-%m-%d_%H-%M-%S.png"
debug { debug {
render-drm-device "/dev/dri/renderD128" render-drm-device "/dev/dri/renderD129"
} }
// gestures { // gestures {
+5 -1
View File
@@ -1 +1,5 @@
environment {
__NV_PRIME_RENDER_OFFLOAD "1"
__VK_LAYER_NV_optimus "NVIDIA_only"
__GLX_VENDOR_LIBRARY_NAME "nvidia"
}
@@ -10,6 +10,10 @@ Item {
RecordService.startOrStop(); RecordService.startOrStop();
} }
function saveReplay() {
RecordService.stopReplay();
}
target: "recording" target: "recording"
} }
@@ -7,18 +7,33 @@ import qs.Utils
pragma Singleton pragma Singleton
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 readonly property string recordingDir: Paths.recordingDir
property bool isRecording: false property bool isRecording: false
property bool isReplayInitStarted: false
property bool isReplayStarted: false
property bool isStopping: false property bool isStopping: false
property bool isReplayStopping: false
readonly property string codec: "libx264" readonly property string codec: "libx264"
readonly property string container: "mkv" readonly property string container: "mkv"
readonly property string pixelFormat: "yuv420p" readonly property string pixelFormat: "yuv420p"
property string recordingDisplay: "" property string recordingDisplay: ""
readonly property int replayDuration: 15
readonly property int framerate: 60 readonly property int framerate: 60
readonly property var codecParams: Object.freeze(["preset=ultrafast", "crf=15", "tune=zerolatency", "color_range=tv"]) readonly property var codecParams: Object.freeze(["preset=ultrafast", "crf=15", "tune=zerolatency", "color_range=tv"])
readonly property var filterArgs: "" readonly property var filterArgs: ""
function getFilename() { function getFilename(prefix = "recording") {
var d = new Date(); var d = new Date();
var year = d.getFullYear(); var year = d.getFullYear();
var month = ("0" + (d.getMonth() + 1)).slice(-2); var month = ("0" + (d.getMonth() + 1)).slice(-2);
@@ -26,7 +41,7 @@ Singleton {
var hours = ("0" + d.getHours()).slice(-2); var hours = ("0" + d.getHours()).slice(-2);
var minutes = ("0" + d.getMinutes()).slice(-2); var minutes = ("0" + d.getMinutes()).slice(-2);
var seconds = ("0" + d.getSeconds()).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() { function getAudioSink() {
@@ -37,6 +52,15 @@ Singleton {
return Niri.focusedOutput || null; 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() { function startOrStop() {
if (isRecording) if (isRecording)
stop(); stop();
@@ -76,7 +100,7 @@ Singleton {
Logger.e("RecordService", "No audio sink available."); Logger.e("RecordService", "No audio sink available.");
return ; 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]; recordProcess.command = ["wf-recorder", "--audio=" + audioSink, "-o", source, "--codec", codec, "--pixel-format", pixelFormat, "--framerate", framerate.toString(), "-f", recordProcess.filePath];
for (const param of codecParams) { for (const param of codecParams) {
recordProcess.command.push("-p"); recordProcess.command.push("-p");
@@ -87,39 +111,78 @@ Singleton {
recordProcess.command.push(filterArgs); recordProcess.command.push(filterArgs);
} }
Logger.i("RecordService", "Starting recording with command: " + recordProcess.command.join(" ")); 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; recordProcess.running = true;
SendNotification.show("Recording started", "Recording to " + recordProcess.filePath); 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 { Process {
id: recordProcess id: recordProcess
property string filePath: "" property string filePath: ""
property var onNormalExit: null
property var onErrorExit: null
running: false running: false
onExited: function(exitCode, exitStatus) { onExited: function(exitCode, exitStatus) {
if (exitCode === 0) { if (exitCode === 0) {
Logger.i("RecordService", "Recording stopped successfully."); Logger.i("RecordService", "Recording stopped successfully.");
if (onNormalExit) { SendNotification.show("Recording stopped", "File saved to: " + filePath);
onNormalExit();
onNormalExit = null;
}
} else { } else {
Logger.e("RecordService", "Recording process exited with error code: " + exitCode); Logger.e("RecordService", "Recording process exited with error code: " + exitCode);
if (onErrorExit) { SendNotification.show("Recording failed", "An error occurred while trying to record the screen.");
onErrorExit();
onErrorExit = null;
}
} }
isRecording = false; isRecording = false;
isStopping = 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;
}
}
}
} }
@@ -3,11 +3,16 @@
pid=$(pgrep -x quickshell) pid=$(pgrep -x quickshell)
[ -z "$pid" ] && exit 1 [ -z "$pid" ] && exit 1
for child in $(pgrep -P "$pid" 2>/dev/null); do # for child in $(pgrep -P "$pid" 2>/dev/null); do
kill "$child" # kill "$child"
done # done
sleep 0.3 children=$(pgrep -P "$pid" 2>/dev/null)
kill "$pid" kill "$pid"
sleep 0.5
for child in $children; do
kill "$child" || true
done
+1 -1
View File
@@ -9,7 +9,7 @@
# Constants # Constants
niri_config_file="$HOME/.config/niri/config/misc.kdl" niri_config_file="$HOME/.config/niri/config/misc.kdl"
prefer_order=(intel nvidia) prefer_order=(nvidia intel)
# Get vendor and path of each GPU # Get vendor and path of each GPU
default_card_path="$(find /dev/dri/card* 2>/dev/null | head -n 1)" default_card_path="$(find /dev/dri/card* 2>/dev/null | head -n 1)"