#!/usr/bin/env bash # rice-capture - screenshots, recordings, OCR, QR codes, colours, webcam, transcoding. # # rice-capture region flameshot as usual: drag, annotate, Ctrl+C copies, Ctrl+S saves # rice-capture screen the whole screen, saved and copied # rice-capture text copy the text inside a region (OCR) # rice-capture qr copy what a QR code inside a region says # rice-capture color copy the colour under the cursor # rice-capture record start a recording (asks what to record), or stop the running one # rice-capture webcam webcam overlay on/off, for recordings # rice-capture transcode shrink a recent picture or video for sharing # # Full-screen shots land in Pictures/Screenshots, recordings in Videos/Recordings. # Recordings and transcodes end with a notification: click it to open the file. set -uo pipefail run="${XDG_RUNTIME_DIR:-/tmp}/rice" mkdir -p "$run" pics="$(xdg-user-dir PICTURES 2>/dev/null || echo "$HOME/Pictures")"; pics="${pics%/}" vids="$(xdg-user-dir VIDEOS 2>/dev/null || echo "$HOME/Videos")"; vids="${vids%/}" shots="$pics/Screenshots" recordings="$vids/Recordings" stamp() { date +%Y-%m-%d_%H-%M-%S; } menu() { rofi -dmenu -i -no-custom -format i -theme "$HOME/.config/rofi/menu.rasi" -p "$1"; } # The selection rectangle uses the theme's accent. slop_opts() { local hex; hex=$(rice-theme get accent); hex=${hex#\#} awk -v r=$((16#${hex:0:2})) -v g=$((16#${hex:2:2})) -v b=$((16#${hex:4:2})) \ 'BEGIN { printf "-b 2 -l -c %.2f,%.2f,%.2f,0.18", r / 255, g / 255, b / 255 }' } # Select a region into a temp PNG; prints its path, or nothing if cancelled. grab_region() { local img; img=$(mktemp --suffix .png -p "$run") # shellcheck disable=SC2046 maim -s -u $(slop_opts) "$img" 2>/dev/null if [[ -s $img ]]; then echo "$img"; else rm -f "$img"; fi } # announce [icon] - notification whose click opens the file. announce() { local file="$1" title="$2" icon="${3:-$1}" ( action=$(notify-send -a rice -i "$icon" -A default=Open -A folder="Show in folder" -A delete=Delete \ "$title" "$(basename "$file") · copied") case "$action" in default) xdg-open "$file" ;; folder) thunar "$(dirname "$file")" ;; delete) rm -f -- "$file" && notify-send -a rice -u low "Deleted" "$(basename "$file")" ;; esac ) >/dev/null 2>&1 & } # Screenshots reach the clipboard through flameshot, never xclip: flameshot (Qt) # answers paste requests properly, while pasting a PNG held by xclip crashed # Brave and st. Any flag on `flameshot gui` (--raw, --path, --clipboard) also # swaps its Copy/Save/Pin buttons and Ctrl+C for a single accept button. region() { flameshot gui } screen() { mkdir -p "$shots" flameshot full --clipboard --path "$shots/screenshot-$(stamp).png" } text() { command -v tesseract >/dev/null || { rice-notify send "Text capture" "Install tesseract to read text"; exit 1; } local img out img=$(grab_region); [[ -n $img ]] || exit 0 out=$(tesseract "$img" stdout -l "${RICE_OCR_LANGS:-eng}" --psm 6 2>/dev/null | sed -e 's/[[:space:]]*$//' -e '/./,$!d') rm -f "$img" [[ -n $out ]] || { rice-notify send "Text capture" "No text found in that region"; exit 1; } printf '%s' "$out" | xclip -selection clipboard rice-notify send "Text copied" "$(head -c 160 <<<"$out")" } qr() { local img out img=$(grab_region); [[ -n $img ]] || exit 0 out=$(zbarimg -q --raw -Sdisable -Sqrcode.enable "$img" 2>/dev/null) rm -f "$img" [[ -n $out ]] || { rice-notify send "QR code" "No QR code in that region"; exit 1; } printf '%s' "$out" | xclip -selection clipboard rice-notify send "QR code copied" "It is on the clipboard (and in clipboard history)" } color() { local hex swatch="$run/swatch.png" hex=$(xcolor --format hex 2>/dev/null) || exit 0 [[ $hex =~ ^#[0-9a-fA-F]{6}$ ]] || exit 0 printf '%s' "$hex" | xclip -selection clipboard magick -size 64x64 "xc:$hex" "$swatch" 2>/dev/null notify-send -a rice -u low -i "$swatch" "Colour copied" "$hex" } record() { if pgrep -x gpu-screen-reco >/dev/null; then local file thumb="$run/recording.jpg" file=$(cat "$run/recording.path" 2>/dev/null) pkill -INT -x gpu-screen-reco for _ in {1..80}; do pgrep -x gpu-screen-reco >/dev/null || break; sleep 0.1; done rm -f "$run/recording" if [[ -s $file ]]; then ffmpeg -y -loglevel error -ss 1 -i "$file" -frames:v 1 -vf scale=320:-1 "$thumb" 2>/dev/null printf 'file://%s\n' "$file" | xclip -selection clipboard -t text/uri-list announce "$file" "Recording saved" "$thumb" fi return fi local audio target file args audio=$(printf '%s\n' "󰝟 No audio" "󰕾 Desktop audio" "󰍬 Desktop audio and microphone" | menu "Record") || exit 0 target=$(printf '%s\n' "󰍹 Whole screen" "󰩭 A region (drag it)" "󰖯 One window (click it)" | menu "Record") || exit 0 mkdir -p "$recordings" file="$recordings/recording-$(stamp).mp4" args=(-f 60 -c mp4 -o "$file") case "$audio" in 1) args+=(-a default_output) ;; 2) args+=(-a "default_output|default_input") ;; esac case "$target" in 0) # gpu-screen-recorder 6 records a monitor by name: pick the one under the pointer. local X Y mon eval "$(xdotool getmouselocation --shell 2>/dev/null)" mon=$(xrandr --query | awk -v x="${X:-0}" -v y="${Y:-0}" ' / connected/ { for (i = 3; i <= NF; i++) if ($i ~ /^[0-9]+x[0-9]+\+[0-9]+\+[0-9]+$/) { split($i, g, /[x+]/) if (x >= g[3] && x < g[3] + g[1] && y >= g[4] && y < g[4] + g[2]) { print $1; exit } } }') [[ -n $mon ]] || mon=$(gpu-screen-recorder --list-monitors 2>/dev/null | head -n1 | cut -d'|' -f1) [[ -n $mon ]] || { rice-notify send "Recording" "Found no monitor to record"; exit 1; } args+=(-w "$mon") ;; 1) local geo # shellcheck disable=SC2046 geo=$(slop $(slop_opts) -f '%wx%h+%x+%y' 2>/dev/null) || exit 0 args+=(-w region -region "$geo") ;; 2) local wid wid=$(xdotool selectwindow 2>/dev/null) || exit 0 args+=(-w "$wid") ;; esac echo "$file" > "$run/recording.path" touch "$run/recording" setsid -f gpu-screen-recorder "${args[@]}" > "$run/recording.log" 2>&1 notify-send -a rice -u low -t 1500 "Recording" "Stop with alt + PrtSc or the REC indicator" } webcam() { local pid dev if [[ -f $run/webcam.pid ]] && pid=$(cat "$run/webcam.pid") && kill -0 "$pid" 2>/dev/null; then kill "$pid"; rm -f "$run/webcam.pid"; return fi dev=$(ls /dev/video* 2>/dev/null | head -n1) [[ -n $dev ]] || { rice-notify send "Webcam" "No camera found"; exit 1; } mpv --x11-name=rice-webcam --title=rice-webcam --profile=low-latency --untimed --no-osc --no-audio \ --no-input-default-bindings --input-conf=/dev/null --no-border --ontop \ --geometry=260x260-24-24 --vf=crop=ih:ih "av://v4l2:$dev" >/dev/null 2>&1 & echo $! > "$run/webcam.pid" } transcode() { local files choice file fmt out base mapfile -t files < <(find "$pics" "$vids" -maxdepth 2 -type f \ \( -iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.webp' -o -iname '*.heic' \ -o -iname '*.mp4' -o -iname '*.mov' -o -iname '*.mkv' -o -iname '*.webm' \) \ -printf '%T@\t%p\n' 2>/dev/null | sort -rn | head -n 40 | cut -f2-) (( ${#files[@]} > 0 )) || { rice-notify send "Transcode" "No pictures or videos in Pictures or Videos"; exit 1; } choice=$(for f in "${files[@]}"; do basename "$f"; done | menu "Transcode") || exit 0 file="${files[$choice]}" base="${file%.*}" case "$(file -b --mime-type "$file")" in image/*) fmt=$(printf '%s\n' "jpg · 2160px wide" "jpg · 1080px wide" "png · 1080px wide" | menu "Picture") || exit 0 case "$fmt" in 0) out="$base-2160px.jpg"; magick "$file" -resize '2160x>' -quality 85 -strip "$out" ;; 1) out="$base-1080px.jpg"; magick "$file" -resize '1080x>' -quality 85 -strip "$out" ;; 2) out="$base-1080px.png"; magick "$file" -resize '1080x>' -strip "$out" ;; esac ;; video/*) fmt=$(printf '%s\n' "mp4 · 1080p" "mp4 · 720p" "gif · 720p" | menu "Video") || exit 0 rice-notify send "Transcoding" "$(basename "$file")" case "$fmt" in 0) out="$base-1080p.mp4"; ffmpeg -y -loglevel error -i "$file" -vf "scale=-2:'min(1080,ih)'" -c:v libx264 -crf 23 -preset medium -c:a aac -b:a 128k -movflags +faststart "$out" ;; 1) out="$base-720p.mp4"; ffmpeg -y -loglevel error -i "$file" -vf "scale=-2:'min(720,ih)'" -c:v libx264 -crf 24 -preset medium -c:a aac -b:a 128k -movflags +faststart "$out" ;; 2) out="$base-720p.gif"; ffmpeg -y -loglevel error -i "$file" -vf "fps=15,scale=-2:'min(720,ih)':flags=lanczos,split[a][b];[a]palettegen[p];[b][p]paletteuse" "$out" ;; esac ;; *) rice-notify send "Transcode" "That is not a picture or a video"; exit 1 ;; esac if [[ -s ${out:-} ]]; then printf 'file://%s\n' "$out" | xclip -selection clipboard -t text/uri-list announce "$out" "Transcoded" else rice-notify send "Transcode" "Conversion failed" fi } case "${1:-}" in region|screen|text|qr|color|record|webcam|transcode) "$1" ;; *) sed -n '2,14p' "$0" | sed 's/^# \{0,1\}//'; exit 2 ;; esac