quickshell & script: implemented recording

This commit is contained in:
2025-10-16 23:48:19 +02:00
parent fe38cb5069
commit 753cec4bdc
13 changed files with 343 additions and 21 deletions

View File

@@ -1,6 +1,15 @@
#!/usr/bin/env bash
# https://github.com/end-4/dots-hyprland/blob/main/.config/ags/scripts/record-script.sh
[ -z "$codec" ] && codec="av1_nvenc"
[ -z "$pixel_format" ] && pixel_format="p010le"
[ -z "$frame_rate" ] && frame_rate="60"
[ -z "$codec_params" ] && codec_params=\
"preset=p5 rc=vbr cq=18 \
b:v=80M maxrate=120M bufsize=160M \
color_range=tv"
[ -z "$filter_args" ] && filter_args=""
getdate() {
date '+%Y-%m-%d_%H.%M.%S'
}
@@ -8,23 +17,42 @@ getaudiooutput() {
pactl list sources | grep 'Name' | grep 'monitor' | cut -d ' ' -f2
}
getactivemonitor() {
hyprctl monitors -j | jq -r '.[] | select(.focused == true) | .name'
if [ "$XDG_CURRENT_DESKTOP" = "Hyprland" ]; then
hyprctl monitors -j | jq -r '.[] | select(.focused == true) | .name'
elif [ "$XDG_CURRENT_DESKTOP" = "niri" ]; then
niri msg focused-output | head -n 1 | sed -n 's/.*(\(.*\)).*/\1/p'
fi
}
recorder_args=(
--codec "$codec"
--pixel-format "$pixel_format"
--framerate "$frame_rate"
-f './recording_'"$(getdate)"'.mkv'
)
for param in $codec_params; do
recorder_args+=(-p "$param")
done
for filter in $filter_args; do
recorder_args+=(-F "$filter")
done
mkdir -p "$(xdg-user-dir VIDEOS)"
cd "$(xdg-user-dir VIDEOS)" || exit
if pgrep wf-recorder > /dev/null; then
notify-send "Recording Stopped" "Stopped" -a 'record-script.sh' &
notify-send "Recording Stopped" "Stopped" -a 'record-script' &
pkill wf-recorder &
else
notify-send "Starting recording" 'recording_'"$(getdate)"'.mp4' -a 'record-script.sh'
notify-send "Starting recording" 'recording_'"$(getdate)"'.mkv' -a 'record-script'
if [[ "$1" == "--sound" ]]; then
wf-recorder --pixel-format yuv420p -f './recording_'"$(getdate)"'.mp4' -t --geometry "$(slurp)" --audio="$(getaudiooutput)" & disown
wf-recorder --geometry "$(slurp)" --audio="$(getaudiooutput)" "${recorder_args[@]}" & disown
elif [[ "$1" == "--fullscreen-sound" ]]; then
wf-recorder -o $(getactivemonitor) --pixel-format yuv420p -f './recording_'"$(getdate)"'.mp4' -t --audio="$(getaudiooutput)" & disown
wf-recorder -o "$(getactivemonitor)" --audio="$(getaudiooutput)" "${recorder_args[@]}" & disown
elif [[ "$1" == "--fullscreen" ]]; then
wf-recorder -o $(getactivemonitor) --pixel-format yuv420p -f './recording_'"$(getdate)"'.mp4' -t & disown
wf-recorder -o "$(getactivemonitor)" "${recorder_args[@]}" & disown
else
wf-recorder --pixel-format yuv420p -f './recording_'"$(getdate)"'.mp4' -t --geometry "$(slurp)" & disown
wf-recorder --geometry "$(slurp)" "${recorder_args[@]}" & disown
fi
fi

View File

@@ -238,15 +238,19 @@ window-rule {
open-floating true
}
// Block from recording
window-rule {
match app-id="thunderbird"
block-out-from "screen-capture"
}
// I love round corners
window-rule {
geometry-corner-radius 14
clip-to-geometry true
}
window-rule {
match at-startup=true app-id="Spotify"
open-on-workspace "special"
}
/************************Others************************/
@@ -289,6 +293,7 @@ binds {
Mod+Shift+L { spawn-sh "qs ipc call lyrics toggleBarLyrics"; }
Mod+Shift+K { spawn-sh "pkill -x quickshell || quickshell"; }
Mod+I { spawn-sh "qs ipc call idleInhibitor toggleInhibitor"; }
Mod+Alt+R { spawn-sh "qs ipc call recording startOrStopRecording"; }
// Rofi
Mod+D { spawn-sh "pkill -x rofi || rofi -show run"; }

View File

@@ -238,15 +238,19 @@ window-rule {
open-floating true
}
// Block from recording
window-rule {
match app-id="thunderbird"
block-out-from "screen-capture"
}
// I love round corners
window-rule {
geometry-corner-radius 14
clip-to-geometry true
}
window-rule {
match at-startup=true app-id="Spotify"
open-on-workspace "special"
}
/************************Others************************/
@@ -289,6 +293,7 @@ binds {
Mod+Shift+L { spawn-sh "qs ipc call lyrics toggleBarLyrics"; }
Mod+Shift+K { spawn-sh "pkill -x quickshell || quickshell"; }
Mod+I { spawn-sh "qs ipc call idleInhibitor toggleInhibitor"; }
Mod+Alt+R { spawn-sh "qs ipc call recording startOrStopRecording"; }
// Rofi
Mod+D { spawn-sh "pkill -x rofi || rofi -show run"; }

View File

@@ -2,7 +2,7 @@
"location": "Munich",
"notifications": {
"doNotDisturb": false,
"lastSeenTs": 1760477228000
"lastSeenTs": 1760623982000
},
"primaryColor": "#89b4fa",
"showLyricsBar": false

View File

@@ -36,6 +36,7 @@ Singleton {
readonly property string speedReset: "󰾅"
readonly property string reset: "󰑙"
readonly property string lines: ""
readonly property string record: ""
// Tabler icons
// Expose the font family name for easy access
readonly property string fontFamily: currentFontLoader ? currentFontLoader.name : ""

View File

@@ -172,6 +172,9 @@ Variants {
width: 10
}
RecordIndicator {
}
Ip {
showCountryCode: true
}

View File

@@ -0,0 +1,107 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Constants
import qs.Services
Item {
id: root
property color fillColor: Colors.red
property color _actualColor: Colors.red
visible: RecordService.isRecording
implicitHeight: parent.height
implicitWidth: layout.width + 10
SequentialAnimation {
id: blinkAnimation
running: RecordService.isRecording
loops: Animation.Infinite
ColorAnimation {
target: root
property: "_actualColor"
to: Qt.rgba(fillColor.r, fillColor.g, fillColor.b, 0)
duration: Style.animationSlowest
easing.type: Easing.InOutCubic
}
ColorAnimation {
target: root
property: "_actualColor"
to: fillColor
duration: Style.animationSlowest
easing.type: Easing.InOutCubic
}
}
RowLayout {
id: layout
anchors.top: parent.top
anchors.bottom: parent.bottom
spacing: 0
Text {
text: Icons.record
font.pointSize: Fonts.icon + 6
color: _actualColor
}
Item {
id: expander
implicitWidth: mouseArea.containsMouse ? ipText.implicitWidth + 10 : 0
implicitHeight: parent.height
clip: true
Text {
id: ipText
text: RecordService.recordingDisplay
font.pointSize: Fonts.medium
font.family: Fonts.primary
color: fillColor
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 5
}
Behavior on implicitWidth {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.InOutCubic
}
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: (mouse) => {
if (mouse.button === Qt.LeftButton)
RecordService.startOrStop();
}
}
Behavior on _actualColor {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.InOutCubic
}
}
}

View File

@@ -2,6 +2,7 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.Pipewire
import qs.Constants
import qs.Modules.Panel.Cards
import qs.Noctalia

View File

@@ -8,6 +8,7 @@ Singleton {
id: root
property string cacheDir: Quickshell.env("HOME") + "/.cache/quickshell/"
property string recordingDir: Quickshell.env("HOME") + "/Videos/recordings/"
property var cacheFiles: ["Location.json", "Ip.json", "Notifications.json", "LyricsOffset.txt"]
property bool loaded: false
property string locationCacheFile: cacheDir + "Location.json"
@@ -19,7 +20,7 @@ Singleton {
id: process
running: true
command: ["sh", "-c", `mkdir -p ${cacheDir} && touch ${cacheDir + cacheFiles.join(` && touch ${cacheDir}`)}`]
command: ["sh", "-c", `mkdir -p ${cacheDir} && mkdir -p ${recordingDir} && touch ${cacheDir + cacheFiles.join(` && touch ${cacheDir}`)}`]
onExited: (code, status) => {
if (code === 0)
root.loaded = true;

View File

@@ -40,4 +40,12 @@ Item {
target: "idleInhibitor"
}
IpcHandler {
function startOrStopRecording() {
RecordService.startOrStop();
}
target: "recording"
}
}

View File

@@ -74,7 +74,7 @@ Singleton {
onOffsetChanged: {
if (SettingsService.showLyricsBar)
SendNotification.show("Lyrics Offset Changed", `Current offset: ${offset} ms`, 1000);
SendNotification.show("Lyrics Offset Changed", `Current offset: ${offset} ms`);
writeOffset();
}

View File

@@ -0,0 +1,163 @@
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Services
import qs.Utils
pragma Singleton
Singleton {
property string recordingDir: CacheService.recordingDir
property bool isRecording: false
property bool isStopping: false
property string codec: "av1_nvenc"
property string container: "mkv"
property string pixelFormat: "p010le"
property string recordingDisplay: ""
property int framerate: 60
property var codecParams: ["preset=p5", "rc=vbr", "cq=18", "b:v=80M", "maxrate=120M", "bufsize=160M", "color_range=tv"]
property var filterArgs: []
function getFilename() {
var d = new Date();
var year = d.getFullYear();
var month = ("0" + (d.getMonth() + 1)).slice(-2);
var day = ("0" + d.getDate()).slice(-2);
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;
}
function getAudioSink() {
return AudioService.sink ? AudioService.sink.name + '.monitor' : null; // this works on my machine :)
}
function getVideoSource(callback) {
if (niriFocusedOutputProcess.running) {
Logger.warn("RecordService", "Already fetching focused output, returning null.");
callback(null);
}
niriFocusedOutputProcess.onGetName = callback;
niriFocusedOutputProcess.running = true;
}
function startOrStop() {
if (isRecording)
stop();
else
start();
}
function stop() {
if (!isRecording) {
Logger.warn("RecordService", "Not currently recording, cannot stop.");
return ;
}
if (isStopping) {
Logger.warn("RecordService", "Already stopping, please wait.");
return ;
}
isStopping = true;
recordProcess.signal(15);
}
function start() {
if (isRecording || isStopping) {
Logger.warn("RecordService", "Already recording, cannot start.");
return ;
}
isRecording = true;
getVideoSource((source) => {
if (!source) {
SendNotification.show("Recording failed", "Could not determine which display to record from.");
return ;
}
recordingDisplay = source;
const audioSink = getAudioSink();
if (!audioSink) {
SendNotification.show("Recording failed", "No audio sink available to record from.");
return ;
}
recordProcess.filePath = recordingDir + getFilename();
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");
recordProcess.command.push(param);
}
for (const filter of filterArgs) {
recordProcess.command.push("-F");
recordProcess.command.push(filter);
}
Logger.log("RecordService", "Starting recording with command: " + recordProcess.command.join(" "));
recordProcess.onErrorExit = function() {
SendNotification.show("Recording failed", "An error occurred while trying to record the screen.");
};
recordProcess.onNormalExit = function() {
SendNotification.show("Recording stopped", recordProcess.filePath);
};
recordProcess.running = true;
SendNotification.show("Recording started", "Recording to " + recordProcess.filePath);
});
}
Process {
id: recordProcess
property string filePath: ""
property var onNormalExit: null
property var onErrorExit: null
running: false
onExited: function(exitCode, exitStatus) {
if (exitCode === 0) {
Logger.log("RecordService", "Recording stopped successfully.");
if (onNormalExit) {
onNormalExit();
onNormalExit = null;
}
} else {
Logger.error("RecordService", "Recording process exited with error code: " + exitCode);
if (onErrorExit) {
onErrorExit();
onErrorExit = null;
}
}
isRecording = false;
isStopping = false;
recordingDisplay = "";
}
}
Process {
id: niriFocusedOutputProcess
property var onGetName: null
running: false
command: ["niri", "msg", "focused-output"]
onExited: function(exitCode, exitStatus) {
if (exitCode !== 0) {
Logger.error("RecordService", "Failed to get focused output via niri.");
if (niriFocusedOutputProcess.onGetName) {
niriFocusedOutputProcess.onGetName(null);
niriFocusedOutputProcess.onGetName = null;
}
}
}
stdout: SplitParser {
splitMarker: "\n"
onRead: (data) => {
if (niriFocusedOutputProcess.onGetName) {
const parts = data.split(' ');
const name = parts.length > 0 ? parts[parts.length - 1].slice(1)?.slice(0, -1) : null;
name ? Logger.log("RecordService", "Focused output is: " + name) : Logger.warn("RecordService", "No focused output found.");
niriFocusedOutputProcess.onGetName(name);
niriFocusedOutputProcess.onGetName = null;
}
}
}
}
}

View File

@@ -6,11 +6,11 @@ pragma Singleton
Singleton {
id: root
function show(title, message, timeout = 5000, icon = "", urgency = "normal") {
function show(title, message, icon = "", urgency = "normal") {
if (icon)
Quickshell.execDetached(["notify-send", "-u", urgency, "-i", icon, "-t", timeout.toString(), title, message, "-a", "quickshell"]);
Quickshell.execDetached(["notify-send", "-u", urgency, "-i", icon, title, message, "-a", "quickshell"]);
else
Quickshell.execDetached(["notify-send", "-u", urgency, "-t", timeout.toString(), title, message, "-a", "quickshell"]);
Quickshell.execDetached(["notify-send", "-u", urgency, title, message, "-a", "quickshell"]);
}
}