quickshell: screen recorder card in quick settings

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-08-26 22:52:49 +01:00
parent d725e68d1e
commit 11a856800a

View file

@ -106,6 +106,29 @@ in
pw=$(${pkgs.zenity}/bin/zenity --password --title="WiFi Password" 2>/dev/null) pw=$(${pkgs.zenity}/bin/zenity --password --title="WiFi Password" 2>/dev/null)
[ -n "$pw" ] && ${pkgs.networkmanager}/bin/nmcli device wifi connect "$ssid" password "$pw" [ -n "$pw" ] && ${pkgs.networkmanager}/bin/nmcli device wifi connect "$ssid" password "$pw"
''; '';
# Screen recording, driven by the quick-settings card. The shell execs
# into wf-recorder so the card's SIGINT lands on the recorder itself —
# wf-recorder finalises the mp4 on SIGINT, killing a wrapper instead
# would leave a truncated file. The path is echoed before the exec so
# the card can offer the result. Audio means the default sink's monitor
# (desktop sound), not the microphone.
screenRecScript = pkgs.writeShellScript "screen-rec" ''
out="$HOME/Videos/rec-$(${pkgs.coreutils}/bin/date +%Y%m%d-%H%M%S).mp4"
${pkgs.coreutils}/bin/mkdir -p "$HOME/Videos"
audio=""
if [ "$2" = audio ]; then
sink=$(${pkgs.wireplumber}/bin/wpctl inspect @DEFAULT_AUDIO_SINK@ \
| ${pkgs.gnused}/bin/sed -n 's/.*node\.name = "\(.*\)".*/\1/p')
[ -n "$sink" ] && audio="--audio=$sink.monitor"
fi
if [ "$1" = region ]; then
geom=$(${pkgs.slurp}/bin/slurp) || exit 1
echo "$out"
exec ${pkgs.wf-recorder}/bin/wf-recorder $audio -g "$geom" -f "$out"
fi
echo "$out"
exec ${pkgs.wf-recorder}/bin/wf-recorder $audio -f "$out"
'';
nmcli = "${pkgs.networkmanager}/bin/nmcli"; nmcli = "${pkgs.networkmanager}/bin/nmcli";
# Follow stylix's sans choice so a font swap propagates to the bar # Follow stylix's sans choice so a font swap propagates to the bar
sansFont = osConfig.stylix.fonts.sansSerif.name; sansFont = osConfig.stylix.fonts.sansSerif.name;
@ -1393,6 +1416,7 @@ in
readonly property string askFetch: "${askFetchScript}" readonly property string askFetch: "${askFetchScript}"
readonly property string wxFetch: "${wxFetchScript}" readonly property string wxFetch: "${wxFetchScript}"
readonly property string xdgOpen: "${pkgs.xdg-utils}/bin/xdg-open" readonly property string xdgOpen: "${pkgs.xdg-utils}/bin/xdg-open"
readonly property string screenRec: "${screenRecScript}"
readonly property string themeApply: "${themeApply}" readonly property string themeApply: "${themeApply}"
} }
''; '';
@ -4690,7 +4714,11 @@ in
// centerIn, not verticalCenter: the cell used to be // centerIn, not verticalCenter: the cell used to be
// qsIcon.width so horizontal centring was implicit. // qsIcon.width so horizontal centring was implicit.
anchors.centerIn: parent anchors.centerIn: parent
text: "moon" // Doubles as the recording indicator: with the
// dropdown shut, this dot is the only sign that
// wf-recorder is still running.
text: recCard.recording ? "circle-dot" : "moon"
color: recCard.recording ? Theme.err : Theme.base05
font.pixelSize: 18 font.pixelSize: 18
} }
@ -6223,6 +6251,163 @@ in
anchors.centerIn: parent anchors.centerIn: parent
width: 240 width: 240
spacing: 8 spacing: 8
// Screen recorder. wf-recorder is held as a live
// Process (not execDetached) so Stop can SIGINT it;
// slurp's region pick happens inside the script,
// before the shell execs into the recorder.
Card {
id: recCard
width: parent.width
readonly property bool recording: recProc.running
property bool audio: false
property int elapsed: 0
property string lastFile: ""
function start(mode) {
lastFile = "";
elapsed = 0;
recProc.command = [Commands.screenRec, mode, recCard.audio ? "audio" : "silent"];
recProc.running = true;
// The panel would otherwise be in the shot
// and slurp needs the screen to itself.
qsDropdown.animateClose();
}
Process {
id: recProc
stdout: StdioCollector {
// First line is the path the script chose;
// wf-recorder's own chatter goes to stderr.
onStreamFinished: recCard.lastFile = text.trim().split("\n")[0]
}
}
Timer {
interval: 1000
repeat: true
running: recCard.recording
onTriggered: recCard.elapsed++
}
Item {
width: parent.width
height: 20
Row {
spacing: 6
anchors.verticalCenter: parent.verticalCenter
SIcon {
anchors.verticalCenter: parent.verticalCenter
text: "video"
color: recCard.recording ? Theme.err : Theme.base05
font.pixelSize: 15
}
SText {
anchors.verticalCenter: parent.verticalCenter
text: recCard.recording
? Math.floor(recCard.elapsed / 60) + ":" + ("0" + (recCard.elapsed % 60)).slice(-2)
: "Screen Recording"
font.weight: Font.Medium
}
}
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 6
visible: !recCard.recording
SText {
anchors.verticalCenter: parent.verticalCenter
text: "Audio"
color: Theme.base04
font.pixelSize: 10
}
ToggleSwitch {
anchors.verticalCenter: parent.verticalCenter
on: recCard.audio
onToggled: (v) => recCard.audio = v
}
}
}
Row {
width: parent.width
spacing: 6
visible: !recCard.recording
Repeater {
model: [
{ key: "region", label: "Region" },
{ key: "screen", label: "Full screen" }
]
Rectangle {
required property var modelData
width: (parent.width - 6) / 2
height: 26
radius: Theme.radiusSmall
color: recBtn.containsMouse ? Theme.base02 : Theme.base02t
Behavior on color { ColorAnimation { duration: Theme.animFade } }
border.color: Theme.base03
border.width: 1
SText {
anchors.centerIn: parent
text: modelData.label
font.pixelSize: 11
}
MouseArea {
id: recBtn
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: recCard.start(modelData.key)
}
}
}
}
Rectangle {
visible: recCard.recording
width: parent.width
height: 26
radius: Theme.radiusSmall
color: Theme.err
SText {
anchors.centerIn: parent
text: "Stop"
font.pixelSize: 11
color: Theme.base00
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
// SIGINT, not kill wf-recorder writes the
// container trailer on it.
onClicked: recProc.signal(2)
}
}
SText {
visible: !recCard.recording && recCard.lastFile !== ""
width: parent.width
elide: Text.ElideMiddle
text: "Saved " + recCard.lastFile.split("/").pop()
color: Theme.base04
font.pixelSize: 10
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: Quickshell.execDetached([Commands.xdgOpen, recCard.lastFile])
}
}
}
NightlightCard { width: parent.width } NightlightCard { width: parent.width }
} }
} }