quickshell & script: implemented recording
This commit is contained in:
@@ -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
|
||||
@@ -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"; }
|
||||
|
||||
@@ -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"; }
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"location": "Munich",
|
||||
"notifications": {
|
||||
"doNotDisturb": false,
|
||||
"lastSeenTs": 1760477228000
|
||||
"lastSeenTs": 1760623982000
|
||||
},
|
||||
"primaryColor": "#89b4fa",
|
||||
"showLyricsBar": false
|
||||
|
||||
@@ -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 : ""
|
||||
|
||||
@@ -172,6 +172,9 @@ Variants {
|
||||
width: 10
|
||||
}
|
||||
|
||||
RecordIndicator {
|
||||
}
|
||||
|
||||
Ip {
|
||||
showCountryCode: true
|
||||
}
|
||||
|
||||
107
quickshell/Modules/Bar/Components/RecordIndicator.qml
Normal file
107
quickshell/Modules/Bar/Components/RecordIndicator.qml
Normal 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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -40,4 +40,12 @@ Item {
|
||||
target: "idleInhibitor"
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function startOrStopRecording() {
|
||||
RecordService.startOrStop();
|
||||
}
|
||||
|
||||
target: "recording"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
163
quickshell/Services/RecordService.qml
Normal file
163
quickshell/Services/RecordService.qml
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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"]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user