use quickshell region selector for recording

This commit is contained in:
end-4
2025-10-26 15:45:07 +01:00
parent 53998cc51a
commit 7c5740a39b
8 changed files with 143 additions and 51 deletions
+4 -3
View File
@@ -73,9 +73,10 @@ bindd = Super+Shift, C, Color picker, exec, hyprpicker -a # Pick color (Hex) >>
bindld = ,Print, Screenshot >> clipboard ,exec,grim - | wl-copy # Screenshot >> clipboard
bindld = Ctrl,Print, Screenshot >> clipboard & save, exec, mkdir -p $(xdg-user-dir PICTURES)/Screenshots && grim $(xdg-user-dir PICTURES)/Screenshots/Screenshot_"$(date '+%Y-%m-%d_%H.%M.%S')".png # Screenshot >> clipboard & file
# Recording stuff
bindl = Super+Alt, R, exec, ~/.config/hypr/hyprland/scripts/record.sh # Record region (no sound)
bindl = Ctrl+Alt, R, exec, ~/.config/hypr/hyprland/scripts/record.sh --fullscreen # [hidden] Record screen (no sound)
bindl = Super+Shift+Alt, R, exec, ~/.config/hypr/hyprland/scripts/record.sh --fullscreen-sound # Record screen (with sound)
bindl = Super+Alt, R, global, quickshell:regionRecord # Record region (no sound)
bindl = Super+Alt, R, exec, qs -c $qsConfig ipc call TEST_ALIVE || ~/.config/quickshell/$qsConfig/scripts/videos/record.sh # [hidden] Record region (no sound) (fallback)
bindl = Ctrl+Alt, R, exec, ~/.config/quickshell/$qsConfig/scripts/videos/record.sh --fullscreen # [hidden] Record screen (no sound)
bindl = Super+Shift+Alt, R, exec, ~/.config/quickshell/$qsConfig/scripts/videos/record.sh --fullscreen --sound # Record screen (with sound)
# AI
bindd = Super+Shift+Alt, mouse:273, Generate AI summary for selected text, exec, ~/.config/hypr/hyprland/scripts/ai/primary-buffer-query.sh # AI summary for selected text
@@ -1,42 +0,0 @@
#!/usr/bin/env bash
getdate() {
date '+%Y-%m-%d_%H.%M.%S'
}
getaudiooutput() {
pactl list sources | grep 'Name' | grep 'monitor' | cut -d ' ' -f2
}
getactivemonitor() {
hyprctl monitors -j | jq -r '.[] | select(.focused == true) | .name'
}
xdgvideo="$(xdg-user-dir VIDEOS)"
if [[ $xdgvideo = "$HOME" ]]; then
unset xdgvideo
fi
mkdir -p "${xdgvideo:-$HOME/Videos}"
cd "${xdgvideo:-$HOME/Videos}" || exit
if pgrep wf-recorder > /dev/null; then
notify-send "Recording Stopped" "Stopped" -a 'Recorder' &
pkill wf-recorder &
else
if [[ "$1" == "--fullscreen-sound" ]]; then
notify-send "Starting recording" 'recording_'"$(getdate)"'.mp4' -a 'Recorder' & disown
wf-recorder -o "$(getactivemonitor)" --pixel-format yuv420p -f './recording_'"$(getdate)"'.mp4' -t --audio="$(getaudiooutput)"
elif [[ "$1" == "--fullscreen" ]]; then
notify-send "Starting recording" 'recording_'"$(getdate)"'.mp4' -a 'Recorder' & disown
wf-recorder -o "$(getactivemonitor)" --pixel-format yuv420p -f './recording_'"$(getdate)"'.mp4' -t
else
if ! region="$(slurp 2>&1)"; then
notify-send "Recording cancelled" "Selection was cancelled" -a 'Recorder' & disown
exit 1
fi
notify-send "Starting recording" 'recording_'"$(getdate)"'.mp4' -a 'Recorder' & disown
if [[ "$1" == "--sound" ]]; then
wf-recorder --pixel-format yuv420p -f './recording_'"$(getdate)"'.mp4' -t --geometry "$region" --audio="$(getaudiooutput)"
else
wf-recorder --pixel-format yuv420p -f './recording_'"$(getdate)"'.mp4' -t --geometry "$region"
fi
fi
fi
@@ -41,7 +41,7 @@ Item {
visible: Config.options.bar.utilButtons.showScreenRecord
sourceComponent: CircleUtilButton {
Layout.alignment: Qt.AlignVCenter
onClicked: Quickshell.execDetached(["bash", "-c", "~/.config/hypr/hyprland/scripts/record.sh"])
onClicked: Quickshell.execDetached([Directories.recordScriptPath])
MaterialSymbol {
horizontalAlignment: Qt.AlignHCenter
fill: 1
@@ -42,6 +42,7 @@ Singleton {
property string userAiPrompts: FileUtils.trimFileProtocol(`${Directories.shellConfig}/ai/prompts`)
property string aiChats: FileUtils.trimFileProtocol(`${Directories.state}/user/ai/chats`)
property string aiTranslationScriptPath: FileUtils.trimFileProtocol(`${Directories.scriptPath}/ai/gemini-translate.sh`)
property string recordScriptPath: FileUtils.trimFileProtocol(`${Directories.scriptPath}/videos/record.sh`)
// Cleanup on init
Component.onCompleted: {
Quickshell.execDetached(["mkdir", "-p", `${shellConfig}`])
@@ -44,6 +44,9 @@ Toolbar {
return "image_search";
case RegionSelection.SnipAction.CharRecognition:
return "document_scanner";
case RegionSelection.SnipAction.Record:
case RegionSelection.SnipAction.RecordWithSound:
return "videocam";
default:
return "";
}
@@ -14,6 +14,7 @@ import Qt.labs.synchronizer
PanelWindow {
id: root
visible: false
color: "transparent"
WlrLayershell.namespace: "quickshell:regionSelector"
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
@@ -26,7 +27,7 @@ PanelWindow {
}
// TODO: Ask: sidebar AI; Ocr: tesseract
enum SnipAction { Copy, Edit, Search, CharRecognition }
enum SnipAction { Copy, Edit, Search, CharRecognition, Record, RecordWithSound }
enum SelectionMode { RectCorners, Circle }
property var action: RegionSelection.SnipAction.Copy
property var selectionMode: RegionSelection.SelectionMode.RectCorners
@@ -175,14 +176,35 @@ PanelWindow {
property real regionY: Math.min(dragStartY, draggingY)
Process {
id: screenshotProcess
id: screenshotProc
running: true
command: ["bash", "-c", `mkdir -p '${StringUtils.shellSingleQuoteEscape(root.screenshotDir)}' && grim -o '${StringUtils.shellSingleQuoteEscape(root.screen.name)}' '${StringUtils.shellSingleQuoteEscape(root.screenshotPath)}'`]
onExited: (exitCode, exitStatus) => {
root.visible = true;
if (root.enableContentRegions) imageDetectionProcess.running = true;
root.preparationDone = !checkRecordingProc.running;
}
}
property bool isRecording: root.action === RegionSelection.SnipAction.Record || root.action === RegionSelection.SnipAction.RecordWithSound
property bool recordingShouldStop: false
Process {
id: checkRecordingProc
running: isRecording
command: ["pidof", "wf-recorder"]
onExited: (exitCode, exitStatus) => {
root.preparationDone = !screenshotProc.running
root.recordingShouldStop = (exitCode === 0);
}
}
property bool preparationDone: false
onPreparationDoneChanged: {
if (!preparationDone) return;
if (root.isRecording && root.recordingShouldStop) {
Quickshell.execDetached([Directories.recordScriptPath]);
root.dismiss();
return;
}
root.visible = true;
}
Process {
id: imageDetectionProcess
@@ -221,11 +243,16 @@ PanelWindow {
}
// Set command for action
const rx = Math.round(root.regionX * root.monitorScale);
const ry = Math.round(root.regionY * root.monitorScale);
const rw = Math.round(root.regionWidth * root.monitorScale);
const rh = Math.round(root.regionHeight * root.monitorScale);
const cropBase = `magick ${StringUtils.shellSingleQuoteEscape(root.screenshotPath)} `
+ `-crop ${root.regionWidth * root.monitorScale}x${root.regionHeight * root.monitorScale}+${root.regionX * root.monitorScale}+${root.regionY * root.monitorScale}`
+ `-crop ${rw}x${rh}+${rx}+${ry}`
const cropToStdout = `${cropBase} -`
const cropInPlace = `${cropBase} '${StringUtils.shellSingleQuoteEscape(root.screenshotPath)}'`
const cleanup = `rm '${StringUtils.shellSingleQuoteEscape(root.screenshotPath)}'`
const slurpRegion = `${rx},${ry} ${rw}x${rh}`
const uploadAndGetUrl = (filePath) => {
return `curl -sF files[]=@'${StringUtils.shellSingleQuoteEscape(filePath)}' ${root.fileUploadApiEndpoint} | jq -r '.files[0].url'`
}
@@ -242,6 +269,12 @@ PanelWindow {
case RegionSelection.SnipAction.CharRecognition:
snipProc.command = ["bash", "-c", `${cropInPlace} && tesseract '${StringUtils.shellSingleQuoteEscape(root.screenshotPath)}' stdout -l $(tesseract --list-langs | awk 'NR>1{print $1}' | tr '\\n' '+' | sed 's/\\+$/\\n/') | wl-copy && ${cleanup}`]
break;
case RegionSelection.SnipAction.Record:
snipProc.command = ["bash", "-c", `${Directories.recordScriptPath} --region '${slurpRegion}'`]
break;
case RegionSelection.SnipAction.RecordWithSound:
snipProc.command = ["bash", "-c", `${Directories.recordScriptPath} --region '${slurpRegion}' --sound`]
break;
default:
console.warn("[Region Selector] Unknown snip action, skipping snip.");
root.dismiss();
@@ -62,6 +62,18 @@ Scope {
GlobalStates.regionSelectorOpen = true
}
function record() {
root.action = RegionSelection.SnipAction.Record
root.selectionMode = RegionSelection.SelectionMode.RectCorners
GlobalStates.regionSelectorOpen = true
}
function recordWithSound() {
root.action = RegionSelection.SnipAction.RecordWithSound
root.selectionMode = RegionSelection.SelectionMode.RectCorners
GlobalStates.regionSelectorOpen = true
}
IpcHandler {
target: "region"
@@ -71,10 +83,15 @@ Scope {
function search() {
root.search()
}
function ocr() {
root.ocr()
}
function record() {
root.record()
}
function recordWithSound() {
root.recordWithSound()
}
}
GlobalShortcut {
@@ -92,4 +109,14 @@ Scope {
description: "Recognizes text in the selected region"
onPressed: root.ocr()
}
GlobalShortcut {
name: "regionRecord"
description: "Records the selected region"
onPressed: root.record()
}
GlobalShortcut {
name: "regionRecordWithSound"
description: "Records the selected region with sound"
onPressed: root.recordWithSound()
}
}
+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env bash
getdate() {
date '+%Y-%m-%d_%H.%M.%S'
}
getaudiooutput() {
pactl list sources | grep 'Name' | grep 'monitor' | cut -d ' ' -f2
}
getactivemonitor() {
hyprctl monitors -j | jq -r '.[] | select(.focused == true) | .name'
}
xdgvideo="$(xdg-user-dir VIDEOS)"
if [[ $xdgvideo = "$HOME" ]]; then
unset xdgvideo
fi
mkdir -p "${xdgvideo:-$HOME/Videos}"
cd "${xdgvideo:-$HOME/Videos}" || exit
# parse --region <value> without modifying $@ so other flags like --fullscreen still work
ARGS=("$@")
MANUAL_REGION=""
SOUND_FLAG=0
FULLSCREEN_FLAG=0
for ((i=0;i<${#ARGS[@]};i++)); do
if [[ "${ARGS[i]}" == "--region" ]]; then
if (( i+1 < ${#ARGS[@]} )); then
MANUAL_REGION="${ARGS[i+1]}"
else
notify-send "Recording cancelled" "No region specified for --region" -a 'Recorder' & disown
exit 1
fi
elif [[ "${ARGS[i]}" == "--sound" ]]; then
SOUND_FLAG=1
elif [[ "${ARGS[i]}" == "--fullscreen" ]]; then
FULLSCREEN_FLAG=1
fi
done
if pgrep wf-recorder > /dev/null; then
notify-send "Recording Stopped" "Stopped" -a 'Recorder' &
pkill wf-recorder &
else
if [[ $FULLSCREEN_FLAG -eq 1 ]]; then
notify-send "Starting recording" 'recording_'"$(getdate)"'.mp4' -a 'Recorder' & disown
if [[ $SOUND_FLAG -eq 1 ]]; then
wf-recorder -o "$(getactivemonitor)" --pixel-format yuv420p -f './recording_'"$(getdate)"'.mp4' -t --audio="$(getaudiooutput)"
else
wf-recorder -o "$(getactivemonitor)" --pixel-format yuv420p -f './recording_'"$(getdate)"'.mp4' -t
fi
else
# If a manual region was provided via --region, use it; otherwise run slurp as before.
if [[ -n "$MANUAL_REGION" ]]; then
region="$MANUAL_REGION"
else
if ! region="$(slurp 2>&1)"; then
notify-send "Recording cancelled" "Selection was cancelled" -a 'Recorder' & disown
exit 1
fi
fi
notify-send "Starting recording" 'recording_'"$(getdate)"'.mp4' -a 'Recorder' & disown
if [[ $SOUND_FLAG -eq 1 ]]; then
wf-recorder --pixel-format yuv420p -f './recording_'"$(getdate)"'.mp4' -t --geometry "$region" --audio="$(getaudiooutput)"
else
wf-recorder --pixel-format yuv420p -f './recording_'"$(getdate)"'.mp4' -t --geometry "$region"
fi
fi
fi