Initial commit

This commit is contained in:
Daniel Fichtinger 2025-07-17 22:57:25 -04:00
commit fd68d9f463
10 changed files with 662 additions and 0 deletions

53
recorder/README.md Normal file
View file

@ -0,0 +1,53 @@
# Niri Recorder
Niri recorder is a shell script and waybar module to for creating screen
recordings meant to be easily shared online.
### Dependencies
- `socat`
- `bash`
- `wf-recorder`
- `niri`
- `jq`
- `ffmpeg`
- `slurp`
- `python3` (optional)
- `waybar` (optional)
- `wl-clipboard`
## Recorder
A utility script meant to be triggered via keybinding. It will record your
screen, then automatically compress the recording and copy its URI path to your
clipboard, so you can easily paste it inside applications like Discord and
Matrix. You do not need the Waybar module to use the script.
**Usage**:
`recorder.sh`
1. Run the script once to begin recording. Arguments:
- `screen` \[default]: record entire screen. Select screen if multi-monitor.
- `region`: Select a region to record.
2. Run the script again to stop recording.
- Arguments don't matter this time.
- Compression and copying to clipboard is done automatically.
## Waybar Module
There is an included waybar module. This module shows the current recording
state. You can also use it to start/stop the recording with your mouse. Please
see `recorder_config.jsonc` for an example of how to setup the custom module.
You can also use `test_waybar.sh` to test the module without creating any
recordings. After you've loaded the Waybar module, simply run the script, and
you should see the module responding to socket messages.
## Acknowledgments
- Thanks to [Axlefublr](https://axlefublr.github.io/screen-recording/) for the
method to optimize the compression of the video.

View file

@ -0,0 +1,9 @@
{
"custom/recorder": {
"exec": "/path/to/recorder.py",
"return-type": "json",
"restart-interval": "never",
"on-click": "/path/to/recorder.sh screen",
"on-click-right": "/path/to/recorder.sh region"
}
}

107
recorder/recorder.py Executable file
View file

@ -0,0 +1,107 @@
#!/bin/env python
import os
import socket
import json
import asyncio
# helper prints dict as json string
def p(obj):
print(json.dumps(obj), flush=True)
SOCKET = "/tmp/recorder-sock.sock"
# default tooltip
DEF_TT = "Click to record screen. Right-click to record region."
# default text
DEF_TEXT = "rec"
# Remove socket already exists
try:
os.unlink(SOCKET)
except OSError:
# doesn't exist, we're good
pass
# track singleton delayed task
delayed_task = None
# async function to send a message with delay
async def delayed_msg(delay, message):
try:
await asyncio.sleep(delay)
p(message)
except asyncio.CancelledError:
pass
# function handles message from socket
def handle_message(data: str, loop):
global delayed_task
# always cancel delayed task
# if there's new message
if delayed_task:
delayed_task.cancel()
out = {}
out_s = ""
out_t = ""
# process the message type
if data:
if data == "REC":
out_s = "on"
out_t = "Recording in progress. Click to stop."
elif data == "CMP":
out_s = "compressing"
out_t = "Recording is being compressed."
elif data == "CPD":
out_s = "copied"
out_t = "Recording has been copied to clipboard."
elif data == "STP":
out_s = "done"
out_t = "Recording has been stopped."
elif data == "ERR":
out_s = "error"
out_t = "Recording has encountered an error."
# format the output
out["text"] = f"rec: {out_s}" if out_s != "" else DEF_TEXT
out["tooltip"] = out_t if out_t != "" else DEF_TT
# print to waybar
p(out)
# check if delayed message should be sent afterwards
if data in ["ERR", "CPD", "STP"]:
# probably redundant but... for good measure lol
if delayed_task:
delayed_task.cancel()
# delayed print of default output
delayed_out = {"text": DEF_TEXT, "tooltip": DEF_TT}
delayed_task = loop.create_task(delayed_msg(5, delayed_out))
# start the server
async def server():
# pointer to this event loop
loop = asyncio.get_running_loop()
# open our socket
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as server:
server.bind(SOCKET)
# needed for async
server.setblocking(False)
# only one connection at a time
server.listen(1)
# main loop; runs until waybar exits
while True:
# receive data from socket
conn, _ = await loop.sock_accept(server)
with conn:
# parse string
data = (await loop.sock_recv(conn, 1024)).decode().strip()
# handle the data
handle_message(data, loop)
# main function
async def main():
# start by outputing default contents
p({"text": DEF_TEXT, "tooltip": DEF_TT})
# start the server
await server()
# entry point
asyncio.run(main())

69
recorder/recorder.sh Executable file
View file

@ -0,0 +1,69 @@
#!/bin/env bash
# depends: wf-recorder, libnotify
TEMPDIR="/tmp/niri-recorder"
mkdir -p "$TEMPDIR"
LOCK="$TEMPDIR/lock"
RFMT=".mkv"
OFMT=".mp4"
RAW="$TEMPDIR/raw$RFMT"
OUTDIR="$HOME/Videos/niri-recorder"
mkdir -p "$OUTDIR"
# sends current recording state to socket
function sig {
echo "$1" | socat - UNIX-CONNECT:/tmp/recorder-sock.sock
}
# function generates unique name for recording
function compname {
now=$(date +"%y-%m-%d-%H:%M:%S")
echo "$OUTDIR/$now$OFMT"
}
# First we need to check if the recording
# lockfile exists.
if [[ -f "$LOCK" ]]; then
# Stop the recording
sig "STP"
kill "$(cat "$LOCK")"
# remove lockfile
rm "$LOCK"
outpath="$(compname)"
sig "CMP"
# compress the recording
ffmpeg -y -i "$RAW" -c:v libx264 -preset slow -crf 21 -r 30 -b:v 2M -maxrate 3M -bufsize 4M -c:a aac -b:a 96k -movflags +faststart "$outpath" || (sig "ERR"; exit 1)
# copy URI path to clipboard
wl-copy -t 'text/uri-list' <<<"file://$outpath" || (sig "ERR"; exit 1)
sig "CPD"
# delete the raw recording
rm "$RAW"
else
# count how many monitors are attached
num_mon=$(niri msg --json outputs | jq 'keys | length')
wf_flags="-Dyf"
if [ "$1" = "region" ];then
# select a screen region
sel=$(slurp) || exit 1
wf-recorder -g "$sel" "$wf_flags" "$RAW" &
elif [ "$1" = "screen" ] || [ "$1" = "" ] && (( num_mon > 1 )); then
# select entire screen
sel=$(slurp -o) || exit 1
wf-recorder -g "$sel" "$wf_flags" "$RAW" &
else
# this runs when screen is specified and there's only one monitor
# it also runs with no args bc screen is default
wf-recorder "$wf_flags" "$RAW" &
fi
sig "REC"
# create lockfile
touch "$LOCK"
# save recorder's process id to lockfile
PID=$!
echo "$PID" > "$LOCK"
fi

14
recorder/test-waybar.sh Executable file
View file

@ -0,0 +1,14 @@
#!/bin/env bash
echo "REC" | socat - UNIX-CONNECT:/tmp/recorder-sock.sock; \
sleep 2; \
echo "ERR" | socat - UNIX-CONNECT:/tmp/recorder-sock.sock; \
sleep 2; \
echo "REC" | socat - UNIX-CONNECT:/tmp/recorder-sock.sock; \
sleep 2; \
echo "STP" | socat - UNIX-CONNECT:/tmp/recorder-sock.sock; \
sleep 2; \
echo "CMP" | socat - UNIX-CONNECT:/tmp/recorder-sock.sock; \
sleep 2; \
echo "CPD" | socat - UNIX-CONNECT:/tmp/recorder-sock.sock; \
sleep 2;