dotfiles/bin/rice-capture
Sarthak 7f41cf7a82 replace the old dotfiles with the sakura line rice
The bare $HOME repo is gone. This is now the rice: install.sh, rice-*
scripts in bin/, configs in home/ linked into $HOME, theme templates, st
and the particle wallpaper. Clone to ~/rice and run install.sh.
2026-09-14 19:44:44 +05:30

212 lines
9.1 KiB
Bash
Executable file

#!/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 <file> <title> [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