diff --git a/dots/.config/hypr/hyprland/keybinds.conf b/dots/.config/hypr/hyprland/keybinds.conf index 60bca72af..e2f37e034 100644 --- a/dots/.config/hypr/hyprland/keybinds.conf +++ b/dots/.config/hypr/hyprland/keybinds.conf @@ -73,11 +73,14 @@ 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+Shift, R, global, quickshell:regionRecord # Record region (no sound) +bindl = Super+Shift, R, exec, qs -c $qsConfig ipc call TEST_ALIVE || ~/.config/quickshell/$qsConfig/scripts/videos/record.sh # [hidden] Record region (no sound) (fallback) +bindl = Super+Alt, R, global, quickshell:regionRecord # [hidden] 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 +bindd = Super+Shift+Alt, mouse:273, Generate AI summary for selected text, exec, ~/.config/hypr/hyprland/scripts/ai/primary-buffer-query.sh # [hidden] AI summary for selected text #! ##! Window diff --git a/dots/.config/hypr/hyprland/scripts/record.sh b/dots/.config/hypr/hyprland/scripts/record.sh deleted file mode 100755 index 37435ac15..000000000 --- a/dots/.config/hypr/hyprland/scripts/record.sh +++ /dev/null @@ -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 diff --git a/dots/.config/quickshell/ii/modules/bar/UtilButtons.qml b/dots/.config/quickshell/ii/modules/bar/UtilButtons.qml index d7f73a4e3..cc3eb64a2 100644 --- a/dots/.config/quickshell/ii/modules/bar/UtilButtons.qml +++ b/dots/.config/quickshell/ii/modules/bar/UtilButtons.qml @@ -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 diff --git a/dots/.config/quickshell/ii/modules/common/Appearance.qml b/dots/.config/quickshell/ii/modules/common/Appearance.qml index f4b42cac0..8a0446f0e 100644 --- a/dots/.config/quickshell/ii/modules/common/Appearance.qml +++ b/dots/.config/quickshell/ii/modules/common/Appearance.qml @@ -159,6 +159,8 @@ Singleton { property color colTertiaryContainer: m3colors.m3tertiaryContainer property color colTertiaryContainerHover: ColorUtils.mix(m3colors.m3tertiaryContainer, m3colors.m3onTertiaryContainer, 0.90) property color colTertiaryContainerActive: ColorUtils.mix(m3colors.m3tertiaryContainer, colLayer1Active, 0.54) + property color colOnTertiary: m3colors.m3onTertiary + property color colOnTertiaryContainer: m3colors.m3onTertiaryContainer property color colOnSecondaryContainer: m3colors.m3onSecondaryContainer property color colSurfaceContainerLow: ColorUtils.transparentize(m3colors.m3surfaceContainerLow, root.contentTransparency) property color colSurfaceContainer: ColorUtils.transparentize(m3colors.m3surfaceContainer, root.contentTransparency) diff --git a/dots/.config/quickshell/ii/modules/common/Directories.qml b/dots/.config/quickshell/ii/modules/common/Directories.qml index 94821e857..411ed6c7f 100644 --- a/dots/.config/quickshell/ii/modules/common/Directories.qml +++ b/dots/.config/quickshell/ii/modules/common/Directories.qml @@ -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}`]) diff --git a/dots/.config/quickshell/ii/modules/regionSelector/OptionsToolbar.qml b/dots/.config/quickshell/ii/modules/regionSelector/OptionsToolbar.qml index cc314fbd3..e737e9cac 100644 --- a/dots/.config/quickshell/ii/modules/regionSelector/OptionsToolbar.qml +++ b/dots/.config/quickshell/ii/modules/regionSelector/OptionsToolbar.qml @@ -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 ""; } diff --git a/dots/.config/quickshell/ii/modules/regionSelector/RegionSelection.qml b/dots/.config/quickshell/ii/modules/regionSelector/RegionSelection.qml index 39c45d965..05610f9dd 100644 --- a/dots/.config/quickshell/ii/modules/regionSelector/RegionSelection.qml +++ b/dots/.config/quickshell/ii/modules/regionSelector/RegionSelection.qml @@ -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(); diff --git a/dots/.config/quickshell/ii/modules/regionSelector/RegionSelector.qml b/dots/.config/quickshell/ii/modules/regionSelector/RegionSelector.qml index 0ccb26eb0..40440366a 100644 --- a/dots/.config/quickshell/ii/modules/regionSelector/RegionSelector.qml +++ b/dots/.config/quickshell/ii/modules/regionSelector/RegionSelector.qml @@ -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() + } } diff --git a/dots/.config/quickshell/ii/scripts/videos/record.sh b/dots/.config/quickshell/ii/scripts/videos/record.sh new file mode 100755 index 000000000..794bcf1ba --- /dev/null +++ b/dots/.config/quickshell/ii/scripts/videos/record.sh @@ -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 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