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. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BLLuuQfeDiqFHHSgd2VWrQ
This commit is contained in:
commit
4b203d06bd
104 changed files with 8077 additions and 0 deletions
3
bin/rice-about
Executable file
3
bin/rice-about
Executable file
|
|
@ -0,0 +1,3 @@
|
|||
#!/usr/bin/env bash
|
||||
# rice-about - system details (fastfetch) in a floating terminal. Any key closes it.
|
||||
exec st -c rice-about -e bash -c 'clear; fastfetch; printf "\n \e[2mpress any key\e[0m"; read -rsn1'
|
||||
61
bin/rice-audio
Executable file
61
bin/rice-audio
Executable file
|
|
@ -0,0 +1,61 @@
|
|||
#!/usr/bin/env bash
|
||||
# rice-audio - audio state for the eww panel, and the actions it runs.
|
||||
#
|
||||
# rice-audio watch JSON on every change (eww deflisten)
|
||||
# rice-audio sink <name> default output; playing streams move with it
|
||||
# rice-audio source <name> default input
|
||||
# rice-audio app-volume <index> <pct> volume of one playing app
|
||||
# rice-audio app-mute <index>
|
||||
# rice-audio mic <pct> | mic-mute
|
||||
set -uo pipefail
|
||||
|
||||
snapshot() {
|
||||
jq -cn \
|
||||
--arg dsink "$(pactl get-default-sink 2>/dev/null)" \
|
||||
--arg dsrc "$(pactl get-default-source 2>/dev/null)" \
|
||||
--argjson sinks "$(pactl -f json list sinks 2>/dev/null || echo '[]')" \
|
||||
--argjson sources "$(pactl -f json list sources 2>/dev/null || echo '[]')" \
|
||||
--argjson apps "$(pactl -f json list sink-inputs 2>/dev/null || echo '[]')" '
|
||||
def pct: [.volume[]?.value_percent | rtrimstr("%") | tonumber] | if length > 0 then (add / length | round) else 0 end;
|
||||
def level: {name: (.description // ""), vol: pct, muted: (.mute // false)};
|
||||
{
|
||||
sink: (($sinks | map(select(.name == $dsink)) | .[0]) // {} | level),
|
||||
source: (($sources | map(select(.name == $dsrc)) | .[0]) // {} | level),
|
||||
sinks: [ $sinks[] | {index, name, desc: .description, default: (.name == $dsink)} ],
|
||||
sources: [ $sources[] | select(.name | endswith(".monitor") | not)
|
||||
| {index, name, desc: .description, default: (.name == $dsrc)} ],
|
||||
apps: [ $apps[] | {id: .index,
|
||||
name: (.properties["application.name"] // .properties["media.name"] // "app"),
|
||||
vol: pct, muted: .mute} ]
|
||||
}'
|
||||
}
|
||||
|
||||
watch() {
|
||||
snapshot
|
||||
pactl subscribe 2>/dev/null |
|
||||
grep --line-buffered -E "'(change|new|remove)' on (sink|source|sink-input|server)" |
|
||||
while read -r _; do
|
||||
# A slider drag fires dozens of events; wait for the burst to end.
|
||||
while read -r -t 0.08 _; do :; done
|
||||
snapshot
|
||||
done
|
||||
}
|
||||
|
||||
# Panel buttons pass numeric indexes, never device names, so nothing a
|
||||
# device reports about itself ends up inside a shell command.
|
||||
num() { [[ ${1:-} =~ ^[0-9]+$ ]] || { echo "rice-audio: expected a number, got '${1:-}'" >&2; exit 2; }; }
|
||||
|
||||
case "${1:-}" in
|
||||
watch) watch ;;
|
||||
sink)
|
||||
num "${2:-}"
|
||||
pactl set-default-sink "$2"
|
||||
pactl -f json list sink-inputs | jq -r '.[].index' | while read -r i; do pactl move-sink-input "$i" "$2"; done
|
||||
;;
|
||||
source) num "${2:-}"; pactl set-default-source "$2" ;;
|
||||
app-volume) num "${2:-}"; num "${3:-}"; pactl set-sink-input-volume "$2" "$3%" ;;
|
||||
app-mute) num "${2:-}"; pactl set-sink-input-mute "$2" toggle ;;
|
||||
mic) num "${2:-}"; wpctl set-volume -l 1.0 @DEFAULT_AUDIO_SOURCE@ "$2%" ;;
|
||||
mic-mute) rice-volume mic-mute ;;
|
||||
*) sed -n '2,10p' "$0" | sed 's/^# \{0,1\}//'; exit 2 ;;
|
||||
esac
|
||||
21
bin/rice-bar
Executable file
21
bin/rice-bar
Executable file
|
|
@ -0,0 +1,21 @@
|
|||
#!/usr/bin/env bash
|
||||
# rice-bar - polybar on every connected monitor.
|
||||
#
|
||||
# rice-bar start (re)launch the bar
|
||||
# rice-bar toggle hide or show it without restarting
|
||||
set -uo pipefail
|
||||
|
||||
start() {
|
||||
pkill -u "$UID" -x polybar
|
||||
while pgrep -u "$UID" -x polybar >/dev/null; do sleep 0.1; done
|
||||
local m
|
||||
for m in $(xrandr --query | awk '/ connected/ {print $1}'); do
|
||||
MONITOR="$m" setsid -f polybar --reload top >/dev/null 2>&1
|
||||
done
|
||||
}
|
||||
|
||||
case "${1:-start}" in
|
||||
start) start ;;
|
||||
toggle) if pgrep -u "$UID" -x polybar >/dev/null; then polybar-msg cmd toggle >/dev/null; else start; fi ;;
|
||||
*) echo "usage: rice-bar start|toggle" >&2; exit 2 ;;
|
||||
esac
|
||||
100
bin/rice-bt
Executable file
100
bin/rice-bt
Executable file
|
|
@ -0,0 +1,100 @@
|
|||
#!/usr/bin/env bash
|
||||
# rice-bt - bluetooth state for the eww panel, and the actions it runs.
|
||||
#
|
||||
# rice-bt list JSON: power, discovery, paired and nearby devices
|
||||
# rice-bt power on|off|toggle
|
||||
# rice-bt scan look for new devices for 15 seconds
|
||||
# rice-bt toggle <MAC> connect or disconnect; new devices are paired and trusted first
|
||||
# rice-bt forget <MAC>
|
||||
#
|
||||
# Device names are chosen by the device, so the panel only ever passes MAC
|
||||
# addresses, and every MAC is checked before it is used.
|
||||
set -uo pipefail
|
||||
|
||||
run="${XDG_RUNTIME_DIR:-/tmp}/rice"
|
||||
mkdir -p "$run"
|
||||
|
||||
say() { rice-notify send "$1" "${2:-}" -i bluetooth; }
|
||||
poke() { date +%s%N > "$run/bt.rev"; }
|
||||
mac_ok() { [[ ${1:-} =~ ^([0-9A-F]{2}:){5}[0-9A-F]{2}$ ]] || { echo "rice-bt: not a MAC address: '${1:-}'" >&2; exit 2; }; }
|
||||
powered() { bluetoothctl show 2>/dev/null | grep -q 'Powered: yes'; }
|
||||
|
||||
glyph() {
|
||||
case "$1" in
|
||||
audio-headset|audio-headphones) echo ;;
|
||||
audio-card) echo ;;
|
||||
input-keyboard) echo ;;
|
||||
input-mouse) echo ;;
|
||||
input-gaming) echo ;;
|
||||
phone) echo ;;
|
||||
computer) echo ;;
|
||||
*) echo ;;
|
||||
esac
|
||||
}
|
||||
|
||||
device_json() {
|
||||
local mac="$1" info name icon paired connected battery
|
||||
info=$(bluetoothctl info "$mac" 2>/dev/null)
|
||||
name=$(sed -n 's/^\s*Alias: //p' <<<"$info" | head -n1)
|
||||
icon=$(sed -n 's/^\s*Icon: //p' <<<"$info" | head -n1)
|
||||
paired=$(grep -q 'Paired: yes' <<<"$info" && echo true || echo false)
|
||||
connected=$(grep -q 'Connected: yes' <<<"$info" && echo true || echo false)
|
||||
battery=$(sed -n 's/^\s*Battery Percentage: .*(\([0-9]*\))/\1/p' <<<"$info" | head -n1)
|
||||
jq -cn --arg mac "$mac" --arg name "${name:-$mac}" --arg glyph "$(glyph "$icon")" \
|
||||
--argjson paired "$paired" --argjson connected "$connected" --arg battery "$battery" \
|
||||
'{mac: $mac, name: $name, glyph: $glyph, paired: $paired, connected: $connected,
|
||||
battery: (if $battery == "" then "" else $battery + "%" end),
|
||||
sub: (if $connected then "connected" elif $paired then "paired" else "tap to pair" end)}'
|
||||
}
|
||||
|
||||
list() {
|
||||
local on=false discovering=false paired="[]" nearby="[]" mac
|
||||
if powered; then
|
||||
on=true
|
||||
bluetoothctl show 2>/dev/null | grep -q 'Discovering: yes' && discovering=true
|
||||
paired=$(bluetoothctl devices Paired 2>/dev/null | awk '{ print $2 }' | grep -E '^([0-9A-F]{2}:){5}[0-9A-F]{2}$' |
|
||||
while read -r mac; do device_json "$mac"; done | jq -cs 'sort_by(.connected | not)')
|
||||
nearby=$(comm -23 <(bluetoothctl devices 2>/dev/null | awk '{ print $2 }' | sort) \
|
||||
<(bluetoothctl devices Paired 2>/dev/null | awk '{ print $2 }' | sort) |
|
||||
grep -E '^([0-9A-F]{2}:){5}[0-9A-F]{2}$' | head -n 8 |
|
||||
while read -r mac; do device_json "$mac"; done | jq -cs '[.[] | select(.name != .mac)]')
|
||||
fi
|
||||
jq -cn --argjson powered "$on" --argjson discovering "$discovering" \
|
||||
--argjson paired "$paired" --argjson nearby "$nearby" \
|
||||
'{powered: $powered, discovering: $discovering, paired: $paired, nearby: $nearby,
|
||||
connected: ([$paired[] | select(.connected)] | length)}'
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
list) list ;;
|
||||
power)
|
||||
case "${2:-toggle}" in
|
||||
on) rfkill unblock bluetooth 2>/dev/null; bluetoothctl power on >/dev/null ;;
|
||||
off) bluetoothctl power off >/dev/null ;;
|
||||
*) if powered; then bluetoothctl power off >/dev/null; else rfkill unblock bluetooth 2>/dev/null; bluetoothctl power on >/dev/null; fi ;;
|
||||
esac
|
||||
poke ;;
|
||||
scan)
|
||||
powered || bluetoothctl power on >/dev/null
|
||||
setsid -f bluetoothctl --timeout 15 scan on >/dev/null 2>&1
|
||||
poke ;;
|
||||
toggle)
|
||||
mac_ok "${2:-}"; mac="$2"
|
||||
info=$(bluetoothctl info "$mac" 2>/dev/null)
|
||||
name=$(sed -n 's/^\s*Alias: //p' <<<"$info" | head -n1)
|
||||
if grep -q 'Connected: yes' <<<"$info"; then
|
||||
bluetoothctl disconnect "$mac" >/dev/null && say "Disconnected" "$name"
|
||||
else
|
||||
if ! grep -q 'Paired: yes' <<<"$info"; then
|
||||
bluetoothctl --timeout 20 pair "$mac" >/dev/null 2>&1 || { say "Pairing failed" "$name"; exit 1; }
|
||||
bluetoothctl trust "$mac" >/dev/null
|
||||
fi
|
||||
bluetoothctl --timeout 15 connect "$mac" >/dev/null 2>&1 && say "Connected" "$name" || say "Could not connect" "$name"
|
||||
fi
|
||||
poke ;;
|
||||
forget)
|
||||
mac_ok "${2:-}"
|
||||
bluetoothctl remove "$2" >/dev/null && say "Forgot device" "$2"
|
||||
poke ;;
|
||||
*) sed -n '2,10p' "$0" | sed 's/^# \{0,1\}//'; exit 2 ;;
|
||||
esac
|
||||
219
bin/rice-cal
Executable file
219
bin/rice-cal
Executable file
|
|
@ -0,0 +1,219 @@
|
|||
#!/usr/bin/env bash
|
||||
# rice-cal - data and actions for the calendar panel. Events live in khal,
|
||||
# synced with the CalDAV server by vdirsyncer; reminders come from rice-reminder.
|
||||
#
|
||||
# rice-cal month month grid JSON (ISO weeks, Monday first); marks days with events
|
||||
# rice-cal agenda events and reminders for the selected day
|
||||
# rice-cal select <YYYY-MM-DD> pick a day
|
||||
# rice-cal shift <-1|1> previous / next month
|
||||
# rice-cal today
|
||||
# rice-cal add new event on the selected day, typed in rofi:
|
||||
# "15:00 dentist" "15:00-16:30 review" "holiday" (all day)
|
||||
# timed events remind you 10 minutes before; end with !30m or !no
|
||||
# rice-cal sync sync with the server in the background
|
||||
# rice-cal setup connect a CalDAV server (asks for URL, user, password; run in a terminal)
|
||||
set -uo pipefail
|
||||
|
||||
e() { eww -c "$HOME/.config/eww" "$@"; }
|
||||
var() { e get "$1" 2>/dev/null; }
|
||||
have_khal() { command -v khal >/dev/null && [[ -f ${XDG_CONFIG_HOME:-$HOME/.config}/khal/config ]]; }
|
||||
isodate() { [[ ${1:-} =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]] && date -d "$1" +%F >/dev/null 2>&1; }
|
||||
|
||||
selected() { local d; d=$(var cal_day); isodate "$d" && echo "$d" || date +%F; }
|
||||
offset() { local o; o=$(var cal_offset); [[ $o =~ ^-?[0-9]+$ ]] && echo "$o" || echo 0; }
|
||||
|
||||
reminders() { rice-reminder json 2>/dev/null || echo '[]'; }
|
||||
|
||||
month() {
|
||||
local off="${1:-$(offset)}" sel="${2:-$(selected)}" first start end counts="{}"
|
||||
first=$(date -d "$(date +%Y-%m-01) $off month" +%F)
|
||||
start=$(date -d "$first -$(( $(date -d "$first" +%u) - 1 )) days" +%F)
|
||||
end=$(date -d "$start +42 days" +%F)
|
||||
|
||||
if have_khal; then
|
||||
counts=$(khal list --once --day-format "" --format "{start-date}" "$start" "$end" 2>/dev/null |
|
||||
grep -E '^[0-9]{4}-[0-9]{2}-[0-9]{2}$' | sort | uniq -c |
|
||||
jq -R -s -c 'split("\n") | map(select(length > 0) | ltrimstr(" ") | capture("^ *(?<n>[0-9]+) (?<d>.+)$") | {(.d): (.n | tonumber)}) | add // {}')
|
||||
fi
|
||||
|
||||
for i in $(seq 0 41); do date -d "$start +$i days" '+%F %-d %m %u %V'; done |
|
||||
jq -R -s -c --arg title "$(date -d "$first" '+%B %Y')" --arg today "$(date +%F)" --arg sel "$sel" \
|
||||
--arg month "$(date -d "$first" +%m)" --argjson counts "$counts" --argjson rem "$(reminders)" '
|
||||
($rem | group_by(.date) | map({(.[0].date): length}) | add // {}) as $rc |
|
||||
split("\n") | map(select(length > 0) | split(" ")) as $days |
|
||||
{
|
||||
title: $title,
|
||||
dow: ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"],
|
||||
weeks: [ range(0; 6) as $w | $days[$w * 7:($w + 1) * 7] | {
|
||||
wk: (.[0][4] | tonumber | tostring),
|
||||
days: map({
|
||||
date: .[0], d: .[1],
|
||||
other: (.[2] != $month),
|
||||
weekend: ((.[3] | tonumber) >= 6),
|
||||
today: (.[0] == $today),
|
||||
selected: (.[0] == $sel and .[0] != $today),
|
||||
events: (($counts[.[0]] // 0) + ($rc[.[0]] // 0))
|
||||
})
|
||||
} ]
|
||||
}'
|
||||
}
|
||||
|
||||
agenda() {
|
||||
local day="${1:-$(selected)}" heading items="[]" status="" sep=$'\x1f'
|
||||
heading=$(date -d "$day" '+%A %-d %B')
|
||||
[[ $day == "$(date +%F)" ]] && heading="Today · $heading"
|
||||
|
||||
if have_khal; then
|
||||
items=$(khal list --day-format "" --format "{start-time}${sep}{end-time}${sep}{title}${sep}{location}" "$day" 1d 2>/dev/null |
|
||||
jq -R -s -c --arg sep "$sep" '
|
||||
split("\n") | map(select(length > 0) | split($sep) | {
|
||||
kind: "event", id: "",
|
||||
time: (if .[0] == "" then "all day" else .[0] end),
|
||||
title: .[2],
|
||||
sub: ([(if .[0] != "" and .[1] != "" then "until " + .[1] else empty end),
|
||||
(if (.[3] // "") != "" then .[3] else empty end)] | join(" · "))
|
||||
})')
|
||||
else
|
||||
status="Events need khal set up (run install.sh)"
|
||||
fi
|
||||
|
||||
jq -cn --arg heading "$heading" --arg status "$status" --arg day "$day" \
|
||||
--argjson items "${items:-[]}" --argjson rem "$(reminders)" '
|
||||
{heading: $heading, status: $status,
|
||||
items: ($items + [ $rem[] | select(.date == $day) | {kind: "reminder", id: .id, time: .time, title: .text, sub: "reminder"} ]
|
||||
| sort_by(if .time == "all day" then "00:00" else .time end))}'
|
||||
}
|
||||
|
||||
# Push new data straight into eww so clicks feel instant.
|
||||
refresh() {
|
||||
local off="$1" day="$2"
|
||||
e update cal_offset="$off" cal_day="$day"
|
||||
e update month="$(month "$off" "$day")" agenda="$(agenda "$day")"
|
||||
}
|
||||
|
||||
add() {
|
||||
have_khal || { rice-notify send "Calendar" "Events need khal set up (run install.sh)"; exit 1; }
|
||||
local day input remind=10m start="" end="" title
|
||||
day=$(selected)
|
||||
input=$(rofi -dmenu -theme "$HOME/.config/rofi/prompt.rasi" -p "$(date -d "$day" '+%a %-d %b')" \
|
||||
-mesg "15:00 dentist · 15:00-16:30 review · holiday · end with !30m or !no") || exit 0
|
||||
[[ -n $input ]] || exit 0
|
||||
|
||||
if [[ $input =~ ^(.*[^[:space:]])[[:space:]]+!(no|[0-9]+[mhd])$ ]]; then
|
||||
input="${BASH_REMATCH[1]}"; remind="${BASH_REMATCH[2]}"
|
||||
fi
|
||||
|
||||
if [[ $input =~ ^([0-9]{1,2}:[0-9]{2})(-([0-9]{1,2}:[0-9]{2}))?[[:space:]]+(.+)$ ]]; then
|
||||
start="${BASH_REMATCH[1]}"; end="${BASH_REMATCH[3]}"; title="${BASH_REMATCH[4]}"
|
||||
khal new "$day" "$start" "${end:-1h}" "$title" >/dev/null 2>&1 || { rice-notify send "Calendar" "khal could not add that"; exit 1; }
|
||||
if [[ $remind != no ]]; then
|
||||
local amount="${remind%?}" unit
|
||||
case "${remind: -1}" in m) unit=minutes ;; h) unit=hours ;; d) unit=days ;; esac
|
||||
at=$(date -d "$day $start $amount $unit ago" '+%F %H:%M' 2>/dev/null) &&
|
||||
rice-reminder add "$at" "$title at $start" >/dev/null 2>&1
|
||||
fi
|
||||
else
|
||||
title="$input"
|
||||
khal new "$day" "$title" >/dev/null 2>&1 || { rice-notify send "Calendar" "khal could not add that"; exit 1; }
|
||||
fi
|
||||
|
||||
rice-notify send "Event added" "$title · $(date -d "$day" '+%a %-d %b') ${start:-all day}"
|
||||
setsid -f vdirsyncer sync >/dev/null 2>&1
|
||||
refresh "$(offset)" "$day"
|
||||
}
|
||||
|
||||
setup() {
|
||||
local conf="${XDG_CONFIG_HOME:-$HOME/.config}" share="${XDG_DATA_HOME:-$HOME/.local/share}"
|
||||
local secrets="$conf/rice/secrets" data="$share/calendars" url user pass first
|
||||
command -v vdirsyncer >/dev/null && command -v khal >/dev/null ||
|
||||
{ echo "rice-cal: install khal and vdirsyncer first"; return 1; }
|
||||
|
||||
read -rp "CalDAV URL [https://dav.srtk.in/dav.php/]: " url </dev/tty
|
||||
url="${url:-https://dav.srtk.in/dav.php/}"
|
||||
[[ $url =~ ^https://[^\"[:space:]]+$ ]] || { echo "rice-cal: use an https:// URL"; return 1; }
|
||||
read -rp "Username: " user </dev/tty
|
||||
[[ $user =~ ^[A-Za-z0-9._@+-]+$ ]] || { echo "rice-cal: that username has characters vdirsyncer can't take"; return 1; }
|
||||
read -rsp "Password: " pass </dev/tty; echo
|
||||
|
||||
# The password stays in a private file; the configs only point at it.
|
||||
mkdir -p "$secrets" && chmod 700 "$secrets"
|
||||
(umask 077; printf '%s' "$pass" > "$secrets/caldav")
|
||||
unset pass
|
||||
mkdir -p "$conf/vdirsyncer" "$conf/khal" "$data" "$share/vdirsyncer/status"
|
||||
|
||||
cat > "$conf/vdirsyncer/config" <<EOF
|
||||
# Written by rice-cal setup. The password is read from $secrets/caldav.
|
||||
[general]
|
||||
status_path = "$share/vdirsyncer/status/"
|
||||
|
||||
[pair calendar]
|
||||
a = "calendar_local"
|
||||
b = "calendar_remote"
|
||||
collections = ["from a", "from b"]
|
||||
metadata = ["displayname", "color"]
|
||||
conflict_resolution = "b wins"
|
||||
|
||||
[storage calendar_local]
|
||||
type = "filesystem"
|
||||
path = "$data/"
|
||||
fileext = ".ics"
|
||||
|
||||
[storage calendar_remote]
|
||||
type = "caldav"
|
||||
url = "$url"
|
||||
username = "$user"
|
||||
password.fetch = ["command", "cat", "$secrets/caldav"]
|
||||
EOF
|
||||
|
||||
echo "Connecting to $url ..."
|
||||
yes | vdirsyncer discover calendar >/dev/null || { echo "rice-cal: could not sign in to $url with those details"; return 1; }
|
||||
vdirsyncer metasync >/dev/null 2>&1
|
||||
vdirsyncer sync || { echo "rice-cal: first sync failed"; return 1; }
|
||||
|
||||
first=$(find "$data" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | sort | head -n1)
|
||||
[[ -n $first ]] || { first=personal; mkdir -p "$data/$first"; }
|
||||
|
||||
cat > "$conf/khal/config" <<EOF
|
||||
# Written by rice-cal setup.
|
||||
[calendars]
|
||||
[[synced]]
|
||||
path = $data/*
|
||||
type = discover
|
||||
|
||||
[locale]
|
||||
timeformat = %H:%M
|
||||
dateformat = %Y-%m-%d
|
||||
longdateformat = %Y-%m-%d
|
||||
datetimeformat = %Y-%m-%d %H:%M
|
||||
longdatetimeformat = %Y-%m-%d %H:%M
|
||||
firstweekday = 0
|
||||
|
||||
[default]
|
||||
default_calendar = $first
|
||||
highlight_event_days = True
|
||||
EOF
|
||||
|
||||
systemctl --user enable --now vdirsyncer.timer >/dev/null 2>&1
|
||||
echo "Calendar ready: new events go to '$first', and it syncs every 15 minutes."
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
setup) setup ;;
|
||||
month) month ;;
|
||||
agenda) agenda ;;
|
||||
select)
|
||||
isodate "${2:-}" || { echo "rice-cal: select takes YYYY-MM-DD" >&2; exit 2; }
|
||||
now=$(date +%Y*12+%-m | sed 's/^0*//'); then=$(date -d "$2" +%Y*12+%-m)
|
||||
refresh "$(( then - now ))" "$2"
|
||||
;;
|
||||
shift)
|
||||
[[ ${2:-} =~ ^-?[0-9]+$ ]] || { echo "rice-cal: shift takes a number" >&2; exit 2; }
|
||||
off=$(( $(offset) + $2 ))
|
||||
if (( off == 0 )); then day=$(date +%F); else day=$(date -d "$(date +%Y-%m-01) $off month" +%F); fi
|
||||
refresh "$off" "$day"
|
||||
;;
|
||||
today) refresh 0 "$(date +%F)" ;;
|
||||
add) add ;;
|
||||
sync) setsid -f vdirsyncer sync >/dev/null 2>&1 ;;
|
||||
*) sed -n '2,15p' "$0" | sed 's/^# \{0,1\}//'; exit 2 ;;
|
||||
esac
|
||||
212
bin/rice-capture
Executable file
212
bin/rice-capture
Executable file
|
|
@ -0,0 +1,212 @@
|
|||
#!/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
|
||||
18
bin/rice-clip
Executable file
18
bin/rice-clip
Executable file
|
|
@ -0,0 +1,18 @@
|
|||
#!/usr/bin/env bash
|
||||
# rice-clip copy|paste - one copy/paste shortcut for terminals and GUI apps.
|
||||
# Terminals want Ctrl+Shift+C/V; everything else wants Ctrl+C/V.
|
||||
set -uo pipefail
|
||||
|
||||
class=$(xdotool getactivewindow getwindowclassname 2>/dev/null)
|
||||
case "${class,,}" in
|
||||
st|st-256color|kitty|alacritty|xterm|urxvt|org.wezfurlong.wezterm|com.mitchellh.ghostty|rice-float|rice-scratchpad|rice-about)
|
||||
mods=ctrl+shift ;;
|
||||
*)
|
||||
mods=ctrl ;;
|
||||
esac
|
||||
|
||||
case "${1:-}" in
|
||||
copy) xdotool key --clearmodifiers "$mods+c" ;;
|
||||
paste) xdotool key --clearmodifiers "$mods+v" ;;
|
||||
*) echo "usage: rice-clip copy|paste" >&2; exit 2 ;;
|
||||
esac
|
||||
11
bin/rice-clipboard
Executable file
11
bin/rice-clipboard
Executable file
|
|
@ -0,0 +1,11 @@
|
|||
#!/usr/bin/env bash
|
||||
# rice-clipboard - clipboard history (greenclip). Picking an entry puts it back on
|
||||
# the clipboard, ready for super + v. rofi runs in dmenu mode so the header reads
|
||||
# "Clipboard": custom rofi modes ignore a custom title and show their mode name.
|
||||
pgrep -u "$UID" -x greenclip >/dev/null || setsid -f greenclip daemon >/dev/null 2>&1
|
||||
|
||||
choice=$(greenclip print | sed '/^$/d' |
|
||||
rofi -dmenu -i -no-custom -p "Clipboard" -theme "$HOME/.config/rofi/menu.rasi" \
|
||||
-theme-str 'window { width: 640px; }') || exit 0
|
||||
|
||||
[[ -n $choice ]] && greenclip print "$choice"
|
||||
51
bin/rice-dictate
Executable file
51
bin/rice-dictate
Executable file
|
|
@ -0,0 +1,51 @@
|
|||
#!/usr/bin/env bash
|
||||
# rice-dictate - offline voice typing into the focused window (nerd-dictation).
|
||||
#
|
||||
# rice-dictate toggle start listening, or stop and finish typing
|
||||
# rice-dictate status on / off
|
||||
#
|
||||
# The first run downloads a small English speech model (about 40 MB) into
|
||||
# ~/.config/nerd-dictation/model. Drop a different VOSK model there to change language.
|
||||
set -uo pipefail
|
||||
|
||||
run="${XDG_RUNTIME_DIR:-/tmp}/rice"
|
||||
model_dir="${XDG_CONFIG_HOME:-$HOME/.config}/nerd-dictation/model"
|
||||
model_url="https://alphacephei.com/vosk/models/vosk-model-small-en-us-0.15.zip"
|
||||
mkdir -p "$run"
|
||||
|
||||
listening() { pgrep -u "$UID" -f 'nerd-dictation begin' >/dev/null; }
|
||||
poke() { date +%s%N > "$run/dictation.rev"; }
|
||||
|
||||
fetch_model() {
|
||||
local tmp
|
||||
rice-notify send "Dictation" "Downloading the speech model (about 40 MB)…"
|
||||
tmp=$(mktemp -d) || return 1
|
||||
if curl -fsSL -o "$tmp/model.zip" "$model_url" && unzip -q "$tmp/model.zip" -d "$tmp"; then
|
||||
mkdir -p "$(dirname "$model_dir")"
|
||||
mv "$tmp"/vosk-model-* "$model_dir"
|
||||
rm -rf "$tmp"
|
||||
rice-notify send "Dictation" "Model ready. Press super + ctrl + x again to start."
|
||||
else
|
||||
rm -rf "$tmp"
|
||||
rice-notify send "Dictation" "Could not download the speech model"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
case "${1:-toggle}" in
|
||||
toggle)
|
||||
command -v nerd-dictation >/dev/null || { rice-notify send "Dictation" "Install nerd-dictation-git to dictate"; exit 1; }
|
||||
if listening; then
|
||||
nerd-dictation end
|
||||
poke
|
||||
elif [[ ! -d $model_dir ]]; then
|
||||
fetch_model
|
||||
else
|
||||
setsid -f nerd-dictation begin --simulate-input-tool=XDOTOOL >/dev/null 2>&1
|
||||
poke
|
||||
notify-send -a rice -u low -t 1500 "Dictation" "Listening. super + ctrl + x to stop."
|
||||
fi
|
||||
;;
|
||||
status) listening && echo on || echo off ;;
|
||||
*) sed -n '2,9p' "$0" | sed 's/^# \{0,1\}//'; exit 2 ;;
|
||||
esac
|
||||
112
bin/rice-display
Executable file
112
bin/rice-display
Executable file
|
|
@ -0,0 +1,112 @@
|
|||
#!/usr/bin/env bash
|
||||
# rice-display - monitor state for the eww panel, and the actions it runs.
|
||||
#
|
||||
# rice-display info JSON: output, modes, refresh rates, DDC brightness
|
||||
# rice-display brightness <0-100>
|
||||
# rice-display mode <id> switch resolution (reverts in 15s unless you keep it)
|
||||
# rice-display rate <id> switch refresh rate at the current resolution
|
||||
set -uo pipefail
|
||||
|
||||
run="${XDG_RUNTIME_DIR:-/tmp}/rice"
|
||||
modes_tsv="$run/modes.tsv"
|
||||
rates_tsv="$run/rates.tsv"
|
||||
bright_cache="$run/brightness"
|
||||
mkdir -p "$run"
|
||||
|
||||
say() { rice-notify send "$1" "${2:-}" -i video-display; }
|
||||
|
||||
primary() {
|
||||
xrandr --query | awk '/ connected primary/ { print $1; exit } / connected/ && !first { first = $1 } END { if (first) print first }' | head -n1
|
||||
}
|
||||
|
||||
# "WxH<TAB>rate<TAB>current(1/0)" for every mode of the output, xrandr order.
|
||||
mode_lines() {
|
||||
xrandr --query | awk -v out="$1" '
|
||||
$1 == out { inblock = 1; next }
|
||||
inblock && /^[^ ]/ { exit }
|
||||
inblock {
|
||||
mode = $1
|
||||
for (i = 2; i <= NF; i++) {
|
||||
r = $i; cur = (r ~ /\*/) ? 1 : 0; gsub(/[*+]/, "", r)
|
||||
if (r != "") printf "%s\t%s\t%d\n", mode, r, cur
|
||||
}
|
||||
}'
|
||||
}
|
||||
|
||||
# DDC is slow (up to a second per read), so reads are cached for a minute.
|
||||
brightness() {
|
||||
if [[ -f $bright_cache ]] && (( $(date +%s) - $(stat -c %Y "$bright_cache") < 60 )); then
|
||||
cat "$bright_cache"; return
|
||||
fi
|
||||
local v
|
||||
v=$(ddcutil getvcp 10 --brief 2>/dev/null | awk '$1 == "VCP" { print $4; exit }')
|
||||
[[ -n $v ]] && echo "$v" | tee "$bright_cache"
|
||||
}
|
||||
|
||||
info() {
|
||||
local out cur_mode cur_rate
|
||||
out=$(primary)
|
||||
mode_lines "$out" > "$run/all-modes.tsv"
|
||||
IFS=$'\t' read -r cur_mode cur_rate _ < <(awk -F'\t' '$3 == 1' "$run/all-modes.tsv")
|
||||
|
||||
awk -F'\t' '!seen[$1]++ { print $1 }' "$run/all-modes.tsv" | head -n 6 |
|
||||
awk -v cur="$cur_mode" '{ printf "%d\t%s\t%d\n", NR - 1, $1, ($1 == cur) }' > "$modes_tsv"
|
||||
# One entry per rounded rate (monitors list 120.00, 119.98 and 119.88), keeping the active one.
|
||||
awk -F'\t' -v cur="$cur_mode" '$1 == cur { printf "%d\t%s\t%d\n", n++, $2, $3 }' "$run/all-modes.tsv" |
|
||||
sort -t$'\t' -k3,3nr -k2,2nr | awk -F'\t' '!seen[int($2 + 0.5)]++' |
|
||||
sort -t$'\t' -k2,2nr | head -n 5 > "$rates_tsv"
|
||||
|
||||
local b ddc=false
|
||||
if command -v ddcutil >/dev/null; then
|
||||
b=$(brightness)
|
||||
[[ -n $b ]] && ddc=true
|
||||
fi
|
||||
|
||||
jq -cn --arg output "$out" --arg mode "$cur_mode" --arg rate "$cur_rate" \
|
||||
--argjson ddc "$ddc" --argjson brightness "${b:-0}" \
|
||||
--argjson bar "$(pgrep -x polybar >/dev/null && echo true || echo false)" \
|
||||
--argjson modes "$(jq -R -s -c 'split("\n") | map(select(length > 0) | split("\t") | {id: (.[0] | tonumber), mode: .[1], current: (.[2] == "1")})' "$modes_tsv")" \
|
||||
--argjson rates "$(jq -R -s -c 'split("\n") | map(select(length > 0) | split("\t") | {id: (.[0] | tonumber), label: ((.[1] | tonumber | round | tostring) + "Hz"), current: (.[2] == "1")})' "$rates_tsv")" \
|
||||
'{output: $output, mode: $mode, rate: $rate, ddc: $ddc, brightness: $brightness, bar: $bar, modes: $modes, rates: $rates,
|
||||
ddcHint: "Needs ddcutil and the i2c-dev module"}'
|
||||
}
|
||||
|
||||
# Apply an xrandr change, then revert unless it is confirmed within 15 seconds.
|
||||
apply_with_revert() {
|
||||
local out="$1" old_mode="$2" old_rate="$3" new_mode="$4" new_rate="$5"
|
||||
xrandr --output "$out" --mode "$new_mode" --rate "$new_rate" || { say "Display" "That mode did not apply"; return 1; }
|
||||
local keep
|
||||
keep=$(printf 'Keep %s @ %sHz\nRevert\n' "$new_mode" "$new_rate" |
|
||||
timeout 15 rofi -dmenu -theme "$HOME/.config/rofi/menu.rasi" -p "Display" -mesg "Reverting in 15 seconds") || keep=""
|
||||
if [[ $keep != Keep* ]]; then
|
||||
xrandr --output "$out" --mode "$old_mode" --rate "$old_rate"
|
||||
say "Display" "Reverted to $old_mode @ ${old_rate}Hz"
|
||||
fi
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
info) info ;;
|
||||
brightness)
|
||||
[[ ${2:-} =~ ^[0-9]+$ ]] && (( $2 <= 100 )) || { echo "rice-display: brightness 0-100" >&2; exit 2; }
|
||||
echo "$2" > "$bright_cache"
|
||||
# Sliders fire many values; only the newest one talks to the monitor.
|
||||
pkill -u "$UID" -f 'ddcutil setvcp 10' 2>/dev/null
|
||||
setsid -f ddcutil setvcp 10 "$2" --noverify >/dev/null 2>&1
|
||||
;;
|
||||
mode|rate)
|
||||
[[ ${2:-} =~ ^[0-9]+$ ]] || { echo "rice-display: $1 takes an id" >&2; exit 2; }
|
||||
out=$(primary)
|
||||
IFS=$'\t' read -r old_mode old_rate _ < <(mode_lines "$out" | awk -F'\t' '$3 == 1')
|
||||
if [[ $1 == mode ]]; then
|
||||
new_mode=$(awk -F'\t' -v id="$2" '$1 == id { print $2 }' "$modes_tsv")
|
||||
new_rate=$(mode_lines "$out" | awk -F'\t' -v m="$new_mode" '$1 == m { print $2 }' | sort -nr | head -n1)
|
||||
else
|
||||
new_mode="$old_mode"
|
||||
new_rate=$(awk -F'\t' -v id="$2" '$1 == id { print $2 }' "$rates_tsv")
|
||||
fi
|
||||
[[ -n $new_mode && -n $new_rate ]] || exit 1
|
||||
rice-panel close
|
||||
apply_with_revert "$out" "$old_mode" "$old_rate" "$new_mode" "$new_rate"
|
||||
;;
|
||||
*) sed -n '2,8p' "$0" | sed 's/^# \{0,1\}//'; exit 2 ;;
|
||||
esac
|
||||
12
bin/rice-emoji
Executable file
12
bin/rice-emoji
Executable file
|
|
@ -0,0 +1,12 @@
|
|||
#!/usr/bin/env bash
|
||||
# rice-emoji - emoji picker that types the emoji into the focused window.
|
||||
theme="$HOME/.config/rofi/menu.rasi"
|
||||
if [[ -f /usr/lib/rofi/emoji.so ]]; then
|
||||
# The "Emoji" header comes from config.rasi (emoji { display-name }).
|
||||
exec rofi -modi emoji -show emoji -emoji-mode insert \
|
||||
-theme "$theme" -theme-str 'window { width: 640px; } listview { columns: 2; lines: 12; }'
|
||||
elif command -v rofimoji >/dev/null; then
|
||||
exec rofimoji --action type --selector-args="-theme $theme -p Emoji"
|
||||
else
|
||||
rice-notify send "Emoji" "Install rofi-emoji to use the picker"
|
||||
fi
|
||||
33
bin/rice-font
Executable file
33
bin/rice-font
Executable file
|
|
@ -0,0 +1,33 @@
|
|||
#!/usr/bin/env bash
|
||||
# rice-font [family] - one monospace font for the whole rice: terminals, bar,
|
||||
# menus, popups and notifications. Without an argument, pick from the
|
||||
# installed Nerd Fonts. The choice is kept in ~/.config/rice/theme.override.toml,
|
||||
# so it survives theme changes.
|
||||
set -uo pipefail
|
||||
|
||||
override="${XDG_CONFIG_HOME:-$HOME/.config}/rice/theme.override.toml"
|
||||
current=$(rice-theme get font 2>/dev/null)
|
||||
|
||||
mapfile -t fonts < <(fc-list :spacing=mono family | tr ',' '\n' | grep -i 'nerd font$' | grep -vi 'mono$\|propo$' | sort -u)
|
||||
(( ${#fonts[@]} > 0 )) || { rice-notify send "Font" "No Nerd Fonts installed"; exit 1; }
|
||||
|
||||
if [[ -n ${1:-} ]]; then
|
||||
font="$1"
|
||||
else
|
||||
active=""
|
||||
for i in "${!fonts[@]}"; do [[ ${fonts[$i]} == "$current" ]] && active=$i; done
|
||||
choice=$(printf '%s\n' "${fonts[@]}" |
|
||||
rofi -dmenu -i -no-custom -format i -theme "$HOME/.config/rofi/menu.rasi" -p "Font" ${active:+-a "$active"}) || exit 0
|
||||
font="${fonts[$choice]}"
|
||||
fi
|
||||
|
||||
printf '%s\n' "${fonts[@]}" | grep -Fxq -- "$font" || { rice-notify send "Font" "\"$font\" is not an installed Nerd Font"; exit 1; }
|
||||
|
||||
mkdir -p "$(dirname "$override")"
|
||||
touch "$override"
|
||||
sed -i '/^font *=/d' "$override"
|
||||
printf 'font = "%s"\n' "$font" >> "$override"
|
||||
|
||||
rice-theme reload
|
||||
rice-hook font-set "$font"
|
||||
rice-notify send "Font" "$font · open terminals pick it up when reopened"
|
||||
28
bin/rice-hook
Executable file
28
bin/rice-hook
Executable file
|
|
@ -0,0 +1,28 @@
|
|||
#!/usr/bin/env bash
|
||||
# rice-hook <event> [args...] - run your own scripts when something happens.
|
||||
#
|
||||
# Every executable file in ~/.config/rice/hooks/<event>.d/ runs, in name order,
|
||||
# with the event's arguments. Events:
|
||||
# post-boot the desktop has started
|
||||
# theme-set <name> a theme was applied
|
||||
# font-set <family> the font changed
|
||||
# wallpaper-set <image> the wallpaper changed
|
||||
# lock / unlock the screen locked or unlocked
|
||||
# update install.sh --update finished
|
||||
set -uo pipefail
|
||||
|
||||
event="${1:?usage: rice-hook <event> [args...]}"
|
||||
shift
|
||||
[[ $event =~ ^[a-z-]+$ ]] || { echo "rice-hook: bad event name '$event'" >&2; exit 2; }
|
||||
|
||||
dir="${XDG_CONFIG_HOME:-$HOME/.config}/rice/hooks/$event.d"
|
||||
log="${XDG_STATE_HOME:-$HOME/.local/state}/rice/hooks.log"
|
||||
[[ -d $dir ]] || exit 0
|
||||
mkdir -p "$(dirname "$log")"
|
||||
|
||||
for hook in "$dir"/*; do
|
||||
[[ -f $hook && -x $hook && $hook != *.sample ]] || continue
|
||||
if ! "$hook" "$@" >>"$log" 2>&1; then
|
||||
printf '%s %s failed (exit %s)\n' "$(date -Is)" "$hook" "$?" >>"$log"
|
||||
fi
|
||||
done
|
||||
58
bin/rice-idle
Executable file
58
bin/rice-idle
Executable file
|
|
@ -0,0 +1,58 @@
|
|||
#!/usr/bin/env bash
|
||||
# rice-idle - screensaver, then lock, when you walk away.
|
||||
#
|
||||
# rice-idle start start the idle chain (rice-session does this)
|
||||
# rice-idle toggle stay awake on/off
|
||||
# rice-idle status JSON for the control centre
|
||||
#
|
||||
# Timings live in ~/.config/rice/idle.conf (seconds):
|
||||
# SCREENSAVER=300 idle time before the screensaver
|
||||
# LOCK=300 further idle time before the lock
|
||||
# Nothing fires while a window is fullscreen or audio is playing. Suspending
|
||||
# always locks first.
|
||||
set -uo pipefail
|
||||
|
||||
run="${XDG_RUNTIME_DIR:-/tmp}/rice"
|
||||
mkdir -p "$run"
|
||||
SCREENSAVER=300
|
||||
LOCK=300
|
||||
conf="${XDG_CONFIG_HOME:-$HOME/.config}/rice/idle.conf"
|
||||
[[ -f $conf ]] && . "$conf"
|
||||
|
||||
chain_running() { pgrep -u "$UID" -x xidlehook >/dev/null; }
|
||||
|
||||
start_chain() {
|
||||
xset s off
|
||||
chain_running || setsid -f xidlehook \
|
||||
--not-when-fullscreen --not-when-audio \
|
||||
--timer "$SCREENSAVER" 'rice-screensaver start' 'rice-screensaver stop' \
|
||||
--timer "$LOCK" 'rice-lock' '' >/dev/null 2>&1
|
||||
}
|
||||
|
||||
start_sleep_lock() {
|
||||
pgrep -u "$UID" -x xss-lock >/dev/null || setsid -f xss-lock --transfer-sleep-lock -- rice-lock >/dev/null 2>&1
|
||||
}
|
||||
|
||||
case "${1:-status}" in
|
||||
start)
|
||||
start_sleep_lock
|
||||
[[ -f $run/stay-awake ]] || start_chain
|
||||
;;
|
||||
toggle)
|
||||
if [[ -f $run/stay-awake ]]; then
|
||||
rm -f "$run/stay-awake"
|
||||
start_chain
|
||||
rice-notify send "Stay awake off" "Screensaver after $((SCREENSAVER / 60)) min, lock $((LOCK / 60)) min later"
|
||||
else
|
||||
touch "$run/stay-awake"
|
||||
pkill -u "$UID" -x xidlehook
|
||||
rice-notify send "Stay awake on" "No screensaver or lock until you turn this off"
|
||||
fi
|
||||
;;
|
||||
status)
|
||||
jq -cn --argjson awake "$([[ -f $run/stay-awake ]] && echo true || echo false)" \
|
||||
--argjson screensaver "$SCREENSAVER" --argjson lock "$LOCK" \
|
||||
'{awake: $awake, screensaver: $screensaver, lock: ($screensaver + $lock)}'
|
||||
;;
|
||||
*) sed -n '2,14p' "$0" | sed 's/^# \{0,1\}//'; exit 2 ;;
|
||||
esac
|
||||
27
bin/rice-keys
Executable file
27
bin/rice-keys
Executable file
|
|
@ -0,0 +1,27 @@
|
|||
#!/usr/bin/env bash
|
||||
# rice-keys - searchable cheat sheet of every keybinding, read from sxhkdrc.
|
||||
# A "## Section" line groups bindings; a "# text" line right above a hotkey
|
||||
# describes it.
|
||||
set -uo pipefail
|
||||
|
||||
rc="${XDG_CONFIG_HOME:-$HOME/.config}/sxhkd/sxhkdrc"
|
||||
dim=$(rice-theme get dim)
|
||||
accent=$(rice-theme get accent)
|
||||
|
||||
pretty() {
|
||||
sed -E 's/@//g; s/\{_,/{/g; s/\b(super|ctrl|shift|alt)\b/\u\1/g; s/XF86Audio/Media /g; s/Print/PrtSc/g'
|
||||
}
|
||||
escape() { sed 's/&/\&/g; s/</\</g; s/>/\>/g'; }
|
||||
|
||||
awk '
|
||||
/^## / { section = substr($0, 4); next }
|
||||
/^# / { desc = substr($0, 3); next }
|
||||
/^$/ { desc = ""; next }
|
||||
/^[^#[:space:]]/ && desc != "" { printf "%s\t%s\t%s\n", section, $0, desc; desc = "" }
|
||||
' "$rc" |
|
||||
while IFS=$'\t' read -r section keys desc; do
|
||||
printf '<span foreground="%s">%-30s</span> %s <span foreground="%s">· %s</span>\n' \
|
||||
"$accent" "$(pretty <<<"$keys" | escape)" "$(escape <<<"$desc")" "$dim" "$(escape <<<"$section")"
|
||||
done |
|
||||
rofi -dmenu -i -markup-rows -no-custom -theme "$HOME/.config/rofi/menu.rasi" -p "Keybindings" \
|
||||
-theme-str 'window { width: 900px; } listview { lines: 16; }' >/dev/null
|
||||
29
bin/rice-lock
Executable file
29
bin/rice-lock
Executable file
|
|
@ -0,0 +1,29 @@
|
|||
#!/usr/bin/env bash
|
||||
# rice-lock - lock the screen with xsecurelock, in the rice's colours, with a
|
||||
# random xscreensaver hack behind the password prompt. Notifications are held
|
||||
# back while locked and shown once you unlock.
|
||||
set -uo pipefail
|
||||
|
||||
pgrep -u "$UID" -x xsecurelock >/dev/null && exit 0
|
||||
|
||||
theme="${XDG_STATE_HOME:-$HOME/.local/state}/rice/theme"
|
||||
[[ -f $theme/xsecurelock.env ]] && . "$theme/xsecurelock.env"
|
||||
|
||||
# xsecurelock's screensaver picks a random hack from a folder. Give it one that
|
||||
# holds only the hacks from ~/.config/rice/screensavers (or rice's defaults).
|
||||
hacks="${XDG_RUNTIME_DIR:-/tmp}/rice/hacks"
|
||||
rm -rf "$hacks" && mkdir -p "$hacks"
|
||||
for h in $(rice-screensaver list); do ln -s "/usr/lib/xscreensaver/$h" "$hacks/$h"; done
|
||||
export XSECURELOCK_XSCREENSAVER_PATH="$hacks"
|
||||
|
||||
rice-screensaver stop >/dev/null 2>&1
|
||||
rice-panel close >/dev/null 2>&1
|
||||
|
||||
level=$(dunstctl get-pause-level 2>/dev/null || echo 0)
|
||||
dunstctl set-pause-level 100 >/dev/null 2>&1
|
||||
|
||||
xsecurelock
|
||||
status=$?
|
||||
|
||||
dunstctl set-pause-level "$level" >/dev/null 2>&1
|
||||
exit "$status"
|
||||
75
bin/rice-media
Executable file
75
bin/rice-media
Executable file
|
|
@ -0,0 +1,75 @@
|
|||
#!/usr/bin/env bash
|
||||
# rice-media - now-playing state for the eww panel and bar.
|
||||
#
|
||||
# rice-media watch JSON on every track or play-state change (eww deflisten)
|
||||
# rice-media position JSON {pos, text} for the seek bar (polled while the panel is open)
|
||||
# rice-media seek <seconds>
|
||||
#
|
||||
# Album art is copied into ~/.cache/rice/art under a hash of its URL, so the
|
||||
# panel only ever points at a file name it created itself.
|
||||
set -uo pipefail
|
||||
|
||||
art_dir="${XDG_CACHE_HOME:-$HOME/.cache}/rice/art"
|
||||
mkdir -p "$art_dir"
|
||||
|
||||
clock() { local s=$1; printf '%d:%02d' $((s / 60)) $((s % 60)); }
|
||||
|
||||
art_path() {
|
||||
local url="$1" key file path=""
|
||||
[[ -n $url ]] || return 0
|
||||
key="$url"
|
||||
if [[ $url == file://* ]]; then
|
||||
path="${url#file://}"
|
||||
path=$(printf '%b' "${path//%/\\x}")
|
||||
# Browsers reuse one temp file for every track, so local art is keyed by mtime too.
|
||||
key+=":$(stat -c %Y -- "$path" 2>/dev/null)"
|
||||
fi
|
||||
file="$art_dir/$(printf '%s' "$key" | sha1sum | cut -c1-16)"
|
||||
if [[ ! -s $file ]]; then
|
||||
if [[ -n $path ]]; then
|
||||
cp -- "$path" "$file" 2>/dev/null
|
||||
elif [[ $url == http*://* ]]; then
|
||||
curl -fsSL --max-time 5 -o "$file" -- "$url" 2>/dev/null
|
||||
fi
|
||||
fi
|
||||
[[ -s $file ]] && printf '%s' "$file"
|
||||
}
|
||||
|
||||
emit() {
|
||||
local status="$1" player="$2" title="$3" artist="$4" album="$5" url="$6" length="$7"
|
||||
local secs=$(( ${length:-0} / 1000000 ))
|
||||
jq -cn --arg status "$status" --arg player "$player" --arg title "$title" \
|
||||
--arg artist "$artist" --arg album "$album" --arg art "$(art_path "$url")" \
|
||||
--argjson length "$secs" --arg lengthText "$( (( secs > 0 )) && clock "$secs")" \
|
||||
'{status: $status, player: $player, title: $title, artist: $artist, album: $album,
|
||||
art: $art, length: $length, lengthText: $lengthText}'
|
||||
}
|
||||
|
||||
watch() {
|
||||
# Unit separator, not tab: read collapses runs of whitespace separators, so an
|
||||
# empty field (browsers often send no album) would shift every later field.
|
||||
local sep=$'\x1f'
|
||||
while :; do
|
||||
emit "Stopped" "" "" "" "" "" 0
|
||||
playerctl --follow metadata --format \
|
||||
"{{status}}${sep}{{playerName}}${sep}{{title}}${sep}{{artist}}${sep}{{album}}${sep}{{mpris:artUrl}}${sep}{{mpris:length}}" 2>/dev/null |
|
||||
while IFS="$sep" read -r status player title artist album url length; do
|
||||
emit "${status:-Stopped}" "$player" "$title" "$artist" "$album" "$url" "${length:-0}"
|
||||
done
|
||||
sleep 2
|
||||
done
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
watch) watch ;;
|
||||
position)
|
||||
pos=$(playerctl position 2>/dev/null | cut -d. -f1)
|
||||
pos=${pos:-0}
|
||||
jq -cn --argjson pos "$pos" --arg text "$(clock "$pos")" '{pos: $pos, text: $text}'
|
||||
;;
|
||||
seek)
|
||||
[[ ${2:-} =~ ^[0-9]+$ ]] || { echo "rice-media: seek takes seconds" >&2; exit 2; }
|
||||
playerctl position "$2"
|
||||
;;
|
||||
*) sed -n '2,10p' "$0" | sed 's/^# \{0,1\}//'; exit 2 ;;
|
||||
esac
|
||||
132
bin/rice-menu
Executable file
132
bin/rice-menu
Executable file
|
|
@ -0,0 +1,132 @@
|
|||
#!/usr/bin/env bash
|
||||
# rice-menu <menu> - every rofi menu in the rice.
|
||||
# main power toggles capture style theme wallpaper screensavers setup
|
||||
set -uo pipefail
|
||||
|
||||
R="$HOME/.config/rofi"
|
||||
run="${XDG_RUNTIME_DIR:-/tmp}/rice"
|
||||
RICE_DIR="${RICE_DIR:-$(cd "$(dirname "$(readlink -f "$0")")/.." && pwd)}"
|
||||
|
||||
# show <title> <active-rows> <row>... rows are "glyph|label|command";
|
||||
# active rows (comma-separated indexes) get the accent colour, like panel rows.
|
||||
show() {
|
||||
local title="$1" active="$2"; shift 2
|
||||
local choice row cmd
|
||||
choice=$(for row in "$@"; do IFS='|' read -r g l _ <<<"$row"; printf '%s %s\n' "$g" "$l"; done |
|
||||
rofi -dmenu -i -no-custom -format i -theme "$R/menu.rasi" -p "$title" ${active:+-a "$active"}) || exit 0
|
||||
[[ $choice =~ ^[0-9]+$ ]] || exit 0
|
||||
row="${*:$((choice + 1)):1}"
|
||||
cmd="${row#*|}"; cmd="${cmd#*|}"
|
||||
eval "$cmd"
|
||||
}
|
||||
|
||||
# Open a file in the editor in a floating terminal, then run a follow-up.
|
||||
edit() { setsid -f st -c rice-float -e sh -c "${EDITOR:-nvim} \"\$1\"; ${2:-true}" _ "$1" >/dev/null 2>&1; }
|
||||
|
||||
# Image grid: <title> <command> then lines of "label<TAB>image<TAB>value".
|
||||
grid() {
|
||||
local title="$1" action="$2" lines choice value
|
||||
lines=$(cat)
|
||||
[[ -n $lines ]] || { rice-notify send "$title" "Nothing to pick from"; exit 0; }
|
||||
choice=$(while IFS=$'\t' read -r label image _; do printf '%s\0icon\x1f%s\n' "$label" "$image"; done <<<"$lines" |
|
||||
rofi -dmenu -i -no-custom -format i -show-icons -theme "$R/grid.rasi" -p "$title") || exit 0
|
||||
[[ $choice =~ ^[0-9]+$ ]] || exit 0
|
||||
value=$(sed -n "$((choice + 1))p" <<<"$lines" | cut -f3)
|
||||
"$action" "$value"
|
||||
}
|
||||
|
||||
rice-theme-set() { rice-theme set "$1"; }
|
||||
rice-wallpaper-set() { rice-wallpaper set "$1"; }
|
||||
|
||||
case "${1:-main}" in
|
||||
main)
|
||||
show "Menu" "" \
|
||||
"|Apps|rofi -show drun" \
|
||||
"|Control centre|rice-panel hub" \
|
||||
"|Calendar|rice-panel calendar" \
|
||||
"|Notifications|rice-panel notifications" \
|
||||
"|New reminder|rice-reminder new" \
|
||||
"|Capture|exec rice-menu capture" \
|
||||
"|Toggles|exec rice-menu toggles" \
|
||||
"|Style|exec rice-menu style" \
|
||||
"|Emoji|rice-emoji" \
|
||||
"|Clipboard|rice-clipboard" \
|
||||
"|Keybindings|rice-keys" \
|
||||
"|Setup|exec rice-menu setup" \
|
||||
"|Power|exec rice-menu power"
|
||||
;;
|
||||
power)
|
||||
show "Power" "" \
|
||||
"|Lock|rice-lock" \
|
||||
"|Screensaver|rice-screensaver now" \
|
||||
"|Suspend|systemctl suspend" \
|
||||
"|Log out|rice-power logout" \
|
||||
"|Reboot|rice-power reboot" \
|
||||
"|Shut down|rice-power shutdown"
|
||||
;;
|
||||
toggles)
|
||||
active=()
|
||||
[[ $(rice-notify dnd status) == on ]] && active+=(0)
|
||||
[[ -f $run/stay-awake ]] && active+=(1)
|
||||
pgrep -f 'nerd-dictation begin' >/dev/null && active+=(3)
|
||||
pgrep -x particles >/dev/null && active+=(4)
|
||||
[[ -f $run/webcam.pid ]] && active+=(5)
|
||||
show "Toggles" "$(IFS=,; echo "${active[*]}")" \
|
||||
"|Do not disturb|rice-notify dnd toggle" \
|
||||
"|Stay awake|rice-idle toggle" \
|
||||
"|Bar|rice-bar toggle" \
|
||||
"|Dictation|rice-dictate toggle" \
|
||||
"|Particles|setsid -f particles" \
|
||||
"|Webcam overlay|rice-capture webcam" \
|
||||
"|Drop-down terminal|rice-scratchpad"
|
||||
;;
|
||||
capture)
|
||||
rows=("|Screenshot a region|rice-capture region" "|Screenshot the screen|rice-capture screen")
|
||||
if pgrep -x gpu-screen-reco >/dev/null; then rows+=("|Stop recording|rice-capture record")
|
||||
else rows+=("|Record the screen|rice-capture record"); fi
|
||||
rows+=("|Copy text from a region|rice-capture text" \
|
||||
"|Read a QR code|rice-capture qr" \
|
||||
"|Pick a colour|rice-capture color" \
|
||||
"|Webcam overlay|rice-capture webcam" \
|
||||
"|Transcode a picture or video|rice-capture transcode")
|
||||
show "Capture" "" "${rows[@]}"
|
||||
;;
|
||||
style)
|
||||
show "Style" "" \
|
||||
"|Theme|exec rice-menu theme" \
|
||||
"|Wallpaper|exec rice-menu wallpaper" \
|
||||
"|Font|rice-font" \
|
||||
"|Screensavers|exec rice-menu screensavers" \
|
||||
"|About this machine|rice-about"
|
||||
;;
|
||||
theme)
|
||||
rice-theme list | while read -r t; do
|
||||
dir="$RICE_DIR/themes/$t"; [[ -d $dir ]] || dir="${XDG_CONFIG_HOME:-$HOME/.config}/rice/themes/$t"
|
||||
img="$dir/preview.png"
|
||||
[[ -f $img ]] || img="$RICE_DIR/wallpapers/$(sed -n 's/^wallpaper *= *"\(.*\)"/\1/p' "$dir/colors.toml")"
|
||||
printf '%s\t%s\t%s\n' "$(sed -n 's/^name *= *"\(.*\)"/\1/p' "$dir/colors.toml")" "$(rice-wallpaper thumb "$img")" "$t"
|
||||
done | grid "Theme" rice-theme-set
|
||||
;;
|
||||
wallpaper)
|
||||
rice-wallpaper list | while read -r img; do
|
||||
printf '%s\t%s\t%s\n' "$(basename "${img%.*}")" "$(rice-wallpaper thumb "$img")" "$img"
|
||||
done | grid "Wallpaper" rice-wallpaper-set
|
||||
;;
|
||||
screensavers)
|
||||
mapfile -t hacks < <(rice-screensaver list)
|
||||
rows=(); for h in "${hacks[@]}"; do rows+=("|$h|rice-screensaver now $h"); done
|
||||
show "Screensavers" "" "${rows[@]}"
|
||||
;;
|
||||
setup)
|
||||
show "Setup" "" \
|
||||
"|Edit keybindings|edit \"\$HOME/.config/sxhkd/sxhkdrc\" 'pkill -USR1 -x sxhkd'" \
|
||||
"|Edit bspwm|edit \"\$HOME/.config/bspwm/bspwmrc\" 'bspc wm -r'" \
|
||||
"|Idle timings|edit \"\$HOME/.config/rice/idle.conf\" 'pkill -x xidlehook; rice-idle start'" \
|
||||
"|Screensaver list|edit \"\$HOME/.config/rice/screensavers\"" \
|
||||
"|Machine-specific settings|edit \"\$HOME/.config/rice/local.sh\"" \
|
||||
"|Hooks folder|setsid -f thunar \"\$HOME/.config/rice/hooks\"" \
|
||||
"|Restart rice daemons|rice-session restart" \
|
||||
"|Update the rice|setsid -f st -c rice-float -e sh -c 'cd \"\$1\" && git pull --ff-only && ./install.sh --update; printf \"\\npress enter\"; read -r _' _ \"$RICE_DIR\""
|
||||
;;
|
||||
*) sed -n '2,3p' "$0" | sed 's/^# \{0,1\}//'; exit 2 ;;
|
||||
esac
|
||||
152
bin/rice-net
Executable file
152
bin/rice-net
Executable file
|
|
@ -0,0 +1,152 @@
|
|||
#!/usr/bin/env bash
|
||||
# rice-net - network state for the eww panel, and the actions it runs.
|
||||
#
|
||||
# rice-net list JSON: Wi-Fi radio, active connection, networks, VPNs
|
||||
# rice-net radio on|off|toggle
|
||||
# rice-net connect <id> id from the last list; asks for a password in rofi if needed
|
||||
# rice-net disconnect
|
||||
# rice-net rescan
|
||||
# rice-net vpn <id> bring a VPN / WireGuard connection up or down
|
||||
# rice-net share show the current Wi-Fi network as a QR code
|
||||
#
|
||||
# SSIDs are chosen by whoever broadcasts them, so the panel only ever passes
|
||||
# the numeric id; the SSID is looked up again here from the cached list.
|
||||
set -uo pipefail
|
||||
|
||||
run="${XDG_RUNTIME_DIR:-/tmp}/rice"
|
||||
cache="$run/wifi.tsv"
|
||||
vpncache="$run/vpn.tsv"
|
||||
mkdir -p "$run"
|
||||
|
||||
say() { rice-notify send "$1" "${2:-}" -i network-wireless; }
|
||||
poke() { date +%s%N > "$run/net.rev"; }
|
||||
|
||||
signal_glyph() {
|
||||
local s=$1
|
||||
if (( s >= 75 )); then echo
|
||||
elif (( s >= 50 )); then echo
|
||||
elif (( s >= 25 )); then echo
|
||||
else echo ; fi
|
||||
}
|
||||
|
||||
list() {
|
||||
local radio=false has_wifi=false active_type="" active_name="" active_dev=""
|
||||
nmcli -g TYPE device 2>/dev/null | grep -qx wifi && has_wifi=true
|
||||
[[ $has_wifi == true && $(nmcli -t radio wifi 2>/dev/null) == enabled ]] && radio=true
|
||||
IFS=: read -r active_type active_name active_dev < <(nmcli -g TYPE,STATE,CONNECTION,DEVICE device status 2>/dev/null |
|
||||
awk -F: '$2 == "connected" && ($1 == "wifi" || $1 == "ethernet") { print $1 ":" $3 ":" $4; exit }')
|
||||
|
||||
local known
|
||||
known=$(nmcli -g NAME,TYPE connection show 2>/dev/null | awk -F: '$2 == "802-11-wireless" { print $1 }')
|
||||
|
||||
# IN-USE:SIGNAL:SECURITY:SSID, strongest first, one row per SSID, top 8.
|
||||
: > "$cache"
|
||||
local inuse signal security ssid id=0 seen=$'\n'
|
||||
while IFS=: read -r inuse signal security ssid; do
|
||||
ssid=${ssid//\\:/:}
|
||||
[[ -z $ssid || $seen == *$'\n'"$ssid"$'\n'* ]] && continue
|
||||
seen+="$ssid"$'\n'
|
||||
printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$id" "$ssid" "$signal" "${security:---}" \
|
||||
"$([[ $inuse == '*' ]] && echo 1 || echo 0)" \
|
||||
"$(grep -Fxq -- "$ssid" <<<"$known" && echo 1 || echo 0)" >> "$cache"
|
||||
(( ++id >= 8 )) && break
|
||||
done < <(nmcli -g IN-USE,SIGNAL,SECURITY,SSID device wifi list --rescan no 2>/dev/null | sort -t: -k2,2nr)
|
||||
|
||||
# VPN and WireGuard profiles known to NetworkManager.
|
||||
: > "$vpncache"
|
||||
local active_names name type vid=0
|
||||
active_names=$(nmcli -g NAME connection show --active 2>/dev/null)
|
||||
while IFS=: read -r name type; do
|
||||
[[ $type == vpn || $type == wireguard ]] || continue
|
||||
printf '%s\t%s\t%s\n' "$vid" "$name" "$(grep -Fxq -- "$name" <<<"$active_names" && echo 1 || echo 0)" >> "$vpncache"
|
||||
(( vid++ ))
|
||||
done < <(nmcli -g NAME,TYPE connection show 2>/dev/null)
|
||||
|
||||
jq -cn --argjson radio "$radio" --argjson haswifi "$has_wifi" \
|
||||
--arg atype "$active_type" --arg aname "$active_name" \
|
||||
--argjson wifi "$(while IFS=$'\t' read -r i s sig sec use kn; do
|
||||
jq -cn --argjson id "$i" --arg ssid "$s" --argjson signal "$sig" --arg sec "$sec" \
|
||||
--argjson active "$([[ $use == 1 ]] && echo true || echo false)" \
|
||||
--argjson known "$([[ $kn == 1 ]] && echo true || echo false)" \
|
||||
--arg glyph "$(signal_glyph "$sig")" \
|
||||
'{id: $id, ssid: $ssid, signal: $signal, active: $active, known: $known, glyph: $glyph,
|
||||
sub: ([(if $active then "connected" elif $known then "saved" else empty end),
|
||||
(if $sec == "--" then "open" else " " + $sec end)] | join(" · "))}'
|
||||
done < "$cache" | jq -cs '.')" \
|
||||
--argjson vpn "$(while IFS=$'\t' read -r i n a; do
|
||||
jq -cn --argjson id "$i" --arg name "$n" --argjson active "$([[ $a == 1 ]] && echo true || echo false)" \
|
||||
'{id: $id, name: $name, active: $active}'
|
||||
done < "$vpncache" | jq -cs '.')" \
|
||||
'{radio: $radio,
|
||||
active: {type: $atype, name: $aname},
|
||||
hasWifi: $haswifi,
|
||||
wifi: $wifi, vpn: $vpn}'
|
||||
}
|
||||
|
||||
field() { awk -F'\t' -v id="$1" -v col="$2" '$1 == id { print $col; exit }' "$3"; }
|
||||
|
||||
connect() {
|
||||
[[ ${1:-} =~ ^[0-9]+$ ]] || { echo "rice-net: connect takes an id" >&2; exit 2; }
|
||||
local ssid security known pass
|
||||
ssid=$(field "$1" 2 "$cache"); security=$(field "$1" 4 "$cache"); known=$(field "$1" 6 "$cache")
|
||||
[[ -n $ssid ]] || { say "Wi-Fi" "That network is gone; rescan and try again"; exit 1; }
|
||||
|
||||
if [[ $known == 1 ]]; then
|
||||
nmcli connection up id "$ssid" >/dev/null 2>&1 && say "Connected" "$ssid" || say "Could not connect" "$ssid"
|
||||
elif [[ $security == "--" ]]; then
|
||||
nmcli device wifi connect "$ssid" >/dev/null 2>&1 && say "Connected" "$ssid" || say "Could not connect" "$ssid"
|
||||
elif [[ $security == *802.1X* ]]; then
|
||||
say "Enterprise network" "Opening the connection editor for $ssid"
|
||||
setsid -f nm-connection-editor >/dev/null 2>&1
|
||||
else
|
||||
pass=$(rofi -dmenu -password -theme "$HOME/.config/rofi/prompt.rasi" -p "$ssid" -mesg "Wi-Fi password") || exit 0
|
||||
[[ -n $pass ]] || exit 0
|
||||
local mgmt=wpa-psk
|
||||
[[ $security == *WPA3* && $security != *WPA2* ]] && mgmt=sae
|
||||
# The password reaches nmcli through a file descriptor, not the command line.
|
||||
nmcli connection add type wifi con-name "$ssid" ssid "$ssid" wifi-sec.key-mgmt "$mgmt" >/dev/null 2>&1
|
||||
if nmcli connection up id "$ssid" passwd-file <(printf '802-11-wireless-security.psk:%s\n' "$pass") >/dev/null 2>&1; then
|
||||
say "Connected" "$ssid"
|
||||
else
|
||||
nmcli connection delete id "$ssid" >/dev/null 2>&1
|
||||
say "Could not connect" "Wrong password for $ssid?"
|
||||
fi
|
||||
fi
|
||||
poke
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
list) list ;;
|
||||
radio)
|
||||
case "${2:-toggle}" in
|
||||
on) nmcli radio wifi on ;;
|
||||
off) nmcli radio wifi off ;;
|
||||
*) [[ $(nmcli -t radio wifi) == enabled ]] && nmcli radio wifi off || nmcli radio wifi on ;;
|
||||
esac
|
||||
poke ;;
|
||||
connect) connect "${2:-}" ;;
|
||||
disconnect)
|
||||
dev=$(nmcli -g TYPE,DEVICE,STATE device status | awk -F: '$1 == "wifi" && $3 == "connected" { print $2; exit }')
|
||||
[[ -n $dev ]] && nmcli device disconnect "$dev" >/dev/null && say "Wi-Fi" "Disconnected"
|
||||
poke ;;
|
||||
rescan) nmcli device wifi rescan >/dev/null 2>&1; poke ;;
|
||||
vpn)
|
||||
[[ ${2:-} =~ ^[0-9]+$ ]] || { echo "rice-net: vpn takes an id" >&2; exit 2; }
|
||||
name=$(field "$2" 2 "$vpncache"); active=$(field "$2" 3 "$vpncache")
|
||||
[[ -n $name ]] || exit 1
|
||||
if [[ $active == 1 ]]; then nmcli connection down id "$name" >/dev/null && say "VPN off" "$name"
|
||||
else nmcli connection up id "$name" >/dev/null && say "VPN on" "$name" || say "VPN failed" "$name"; fi
|
||||
poke ;;
|
||||
share)
|
||||
name=$(nmcli -g TYPE,STATE,CONNECTION device status | awk -F: '$1 == "wifi" && $2 == "connected" { print $3; exit }')
|
||||
[[ -n $name ]] || { say "Wi-Fi" "Not connected to a Wi-Fi network"; exit 1; }
|
||||
ssid=$(nmcli -g 802-11-wireless.ssid connection show id "$name")
|
||||
psk=$(nmcli -s -g 802-11-wireless-security.psk connection show id "$name")
|
||||
esc() { sed -e 's/[\\;,:"]/\\&/g' <<<"$1"; }
|
||||
png="$run/wifi-qr.png"
|
||||
printf 'WIFI:T:%s;S:%s;P:%s;;' "$([[ -n $psk ]] && echo WPA || echo nopass)" "$(esc "$ssid")" "$(esc "$psk")" |
|
||||
qrencode -o "$png" -s 10 -m 2 --foreground="$(rice-theme get bg | tr -d '#')" --background="$(rice-theme get fg | tr -d '#')"
|
||||
setsid -f feh --title "Wi-Fi: $ssid" --geometry 420x420 -Z "$png" >/dev/null 2>&1
|
||||
;;
|
||||
*) sed -n '2,11p' "$0" | sed 's/^# \{0,1\}//'; exit 2 ;;
|
||||
esac
|
||||
95
bin/rice-notify
Executable file
95
bin/rice-notify
Executable file
|
|
@ -0,0 +1,95 @@
|
|||
#!/usr/bin/env bash
|
||||
# rice-notify - one front door for notifications.
|
||||
#
|
||||
# rice-notify send <title> [body] [-i icon] [-u low|normal|critical]
|
||||
# rice-notify dismiss | dismiss-all | action | restore
|
||||
# rice-notify dnd on|off|toggle|status
|
||||
# rice-notify remove <id> | clear
|
||||
# rice-notify watch JSON stream for the notification centre (eww)
|
||||
#
|
||||
# Do-not-disturb is dunst pause level 60, not "paused": rules in dunstrc give
|
||||
# rice's own confirmations and critical CLI alerts level 100 so they still show.
|
||||
set -uo pipefail
|
||||
|
||||
DND_LEVEL=60
|
||||
rev="${XDG_RUNTIME_DIR:-/tmp}/rice/notify.rev"
|
||||
mkdir -p "${rev%/*}"
|
||||
|
||||
poke() {
|
||||
date +%s%N > "$rev"
|
||||
pgrep -x polybar >/dev/null && polybar-msg action indicators hook 0 >/dev/null 2>&1
|
||||
return 0
|
||||
}
|
||||
|
||||
dnd_on() { (( $(dunstctl get-pause-level 2>/dev/null || echo 0) > 0 )); }
|
||||
|
||||
history_json() {
|
||||
local uptime
|
||||
uptime=$(cut -d' ' -f1 /proc/uptime)
|
||||
dunstctl history 2>/dev/null | jq -c --argjson up "$uptime" '
|
||||
[ .data[0][]? | {
|
||||
id: .id.data,
|
||||
app: .appname.data,
|
||||
summary: .summary.data,
|
||||
body: (.body.data | gsub("<[^>]*>"; "") | split("\n")
|
||||
| map(gsub("^\\s+|\\s+$"; "") | select(length > 0)) | .[0:2] | join(" · ")
|
||||
| if length > 110 then .[0:109] + "…" else . end),
|
||||
icon: (.icon_path.data // ""),
|
||||
urgency: (.urgency.data | tostring | ascii_upcase),
|
||||
age: (($up - (.timestamp.data / 1000000)) | floor)
|
||||
}
|
||||
| .ago = (if .age < 60 then "now"
|
||||
elif .age < 3600 then "\(.age / 60 | floor)m"
|
||||
elif .age < 86400 then "\(.age / 3600 | floor)h"
|
||||
else "\(.age / 86400 | floor)d" end)
|
||||
]' || echo '[]'
|
||||
}
|
||||
|
||||
snapshot() {
|
||||
local items
|
||||
items=$(history_json)
|
||||
jq -cn --argjson items "$items" \
|
||||
--argjson dnd "$(dnd_on && echo true || echo false)" \
|
||||
--argjson waiting "$(dunstctl count waiting 2>/dev/null || echo 0)" \
|
||||
'{count: ($items | length), dnd: $dnd, waiting: $waiting, items: $items[:8]}'
|
||||
}
|
||||
|
||||
cmd="${1:-}"; shift || true
|
||||
case "$cmd" in
|
||||
send)
|
||||
title="${1:-}"; body="${2:-}"; shift 2 2>/dev/null || shift $#
|
||||
notify-send -a rice "$@" "$title" "$body"
|
||||
;;
|
||||
dismiss) dunstctl close; poke ;;
|
||||
dismiss-all) dunstctl close-all; poke ;;
|
||||
action) dunstctl action 0; poke ;;
|
||||
restore) dunstctl history-pop; poke ;;
|
||||
remove) [[ ${1:-} =~ ^[0-9]+$ ]] && dunstctl history-rm "$1"; poke ;;
|
||||
open) [[ ${1:-} =~ ^[0-9]+$ ]] && dunstctl history-pop "$1"; poke ;;
|
||||
clear) dunstctl history-clear; poke ;;
|
||||
dnd)
|
||||
case "${1:-toggle}" in
|
||||
on) dunstctl set-pause-level "$DND_LEVEL" ;;
|
||||
off) dunstctl set-pause-level 0 ;;
|
||||
toggle) if dnd_on; then dunstctl set-pause-level 0; else dunstctl set-pause-level "$DND_LEVEL"; fi ;;
|
||||
status) dnd_on && echo on || echo off; exit 0 ;;
|
||||
esac
|
||||
if dnd_on; then
|
||||
notify-send -a rice -u low -h string:x-dunst-stack-tag:dnd "Do not disturb" "Only alarms and critical alerts will show"
|
||||
else
|
||||
notify-send -a rice -u low -h string:x-dunst-stack-tag:dnd "Do not disturb off" "$(dunstctl count waiting) held back"
|
||||
fi
|
||||
poke
|
||||
;;
|
||||
watch)
|
||||
[[ -f $rev ]] || poke
|
||||
while :; do
|
||||
snapshot
|
||||
inotifywait -qq -t 5 -e close_write "$rev" 2>/dev/null
|
||||
done
|
||||
;;
|
||||
*)
|
||||
sed -n '2,11p' "$0" | sed 's/^# \{0,1\}//'
|
||||
[[ -z $cmd ]] || exit 2
|
||||
;;
|
||||
esac
|
||||
6
bin/rice-notify-hook
Executable file
6
bin/rice-notify-hook
Executable file
|
|
@ -0,0 +1,6 @@
|
|||
#!/usr/bin/env bash
|
||||
# Run by dunst for every notification ([notify_centre] in dunstrc). It only
|
||||
# marks "something changed" so the notification centre and bar refresh.
|
||||
rev="${XDG_RUNTIME_DIR:-/tmp}/rice/notify.rev"
|
||||
mkdir -p "${rev%/*}"
|
||||
date +%s%N > "$rev"
|
||||
14
bin/rice-osd
Executable file
14
bin/rice-osd
Executable file
|
|
@ -0,0 +1,14 @@
|
|||
#!/usr/bin/env bash
|
||||
# rice-osd <glyph> <label> <percent|text>
|
||||
# An on-screen display that replaces itself in place (dunst [osd] rule).
|
||||
# A number draws dunst's progress bar; anything else is shown as text.
|
||||
glyph="${1:?usage: rice-osd <glyph> <label> <percent|text>}"
|
||||
label="${2:-}"
|
||||
value="${3:-}"
|
||||
tag="osd-${label,,}"
|
||||
|
||||
if [[ $value =~ ^[0-9]+$ ]]; then
|
||||
notify-send -a osd -u low -h "int:value:$value" -h "string:x-dunst-stack-tag:$tag" "$glyph $label $value%"
|
||||
else
|
||||
notify-send -a osd -u low -h "string:x-dunst-stack-tag:$tag" "$glyph $label $value"
|
||||
fi
|
||||
64
bin/rice-panel
Executable file
64
bin/rice-panel
Executable file
|
|
@ -0,0 +1,64 @@
|
|||
#!/usr/bin/env bash
|
||||
# rice-panel <name> | close | list
|
||||
#
|
||||
# Opens one eww popup at a time (opening one closes the other). A transparent
|
||||
# full-screen "closer" window sits underneath, so a click anywhere else closes
|
||||
# it, and a throwaway sxhkd grabs Escape for as long as a panel is open.
|
||||
set -uo pipefail
|
||||
|
||||
cfg="$HOME/.config/eww"
|
||||
panels="hub audio network bluetooth display power media calendar notifications"
|
||||
|
||||
e() { eww -c "$cfg" "$@"; }
|
||||
|
||||
ensure_daemon() {
|
||||
pgrep -u "$UID" -x eww >/dev/null && return 0
|
||||
setsid -f eww -c "$cfg" daemon >/dev/null 2>&1
|
||||
for _ in {1..50}; do e ping >/dev/null 2>&1 && return 0; sleep 0.1; done
|
||||
}
|
||||
|
||||
open_windows() { e active-windows 2>/dev/null | cut -d: -f1; }
|
||||
|
||||
# Line panels up with tiled windows. bspwm draws the border inside the gap, so a
|
||||
# window's outer edge is exactly one gap below the bar and in from the screen's
|
||||
# right edge; ask it for the live numbers instead of hard-coding them.
|
||||
position() {
|
||||
local pt pr gap
|
||||
read -r pt pr gap < <(bspc query -T -m 2>/dev/null |
|
||||
jq -r '[.padding.top, .padding.right, .windowGap] | @tsv' 2>/dev/null)
|
||||
pt=${pt:-37} pr=${pr:-0} gap=${gap:-10}
|
||||
case "$1" in
|
||||
media|calendar) echo "0x$(( pt + gap ))" ;;
|
||||
*) echo "$(( -(pr + gap) ))x$(( pt + gap ))" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
close_all() {
|
||||
local w
|
||||
for w in $(open_windows); do
|
||||
[[ " $panels closer " == *" $w "* ]] && e close "$w" >/dev/null 2>&1
|
||||
done
|
||||
pkill -u "$UID" -f "sxhkd -c $cfg/panel-keys" 2>/dev/null
|
||||
pgrep -u "$UID" -x eww >/dev/null && e update open_panel="" >/dev/null 2>&1
|
||||
return 0
|
||||
}
|
||||
|
||||
case "${1:-close}" in
|
||||
close) close_all ;;
|
||||
list) tr ' ' '\n' <<<"$panels" ;;
|
||||
*)
|
||||
name="$1"
|
||||
[[ " $panels " == *" $name "* ]] || { echo "rice-panel: unknown panel '$name' (try: rice-panel list)" >&2; exit 2; }
|
||||
ensure_daemon
|
||||
if open_windows | grep -qx "$name"; then
|
||||
close_all
|
||||
else
|
||||
close_all
|
||||
[[ $name == calendar ]] && e update cal_offset=0 cal_day=""
|
||||
e update open_panel="$name"
|
||||
e open closer
|
||||
e open "$name" --pos="$(position "$name")"
|
||||
setsid -f sxhkd -c "$cfg/panel-keys" >/dev/null 2>&1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
79
bin/rice-power
Executable file
79
bin/rice-power
Executable file
|
|
@ -0,0 +1,79 @@
|
|||
#!/usr/bin/env bash
|
||||
# rice-power - system stats and power actions for the eww panel and power menu.
|
||||
#
|
||||
# rice-power info JSON: CPU/GPU load and temperature, memory, uptime, profile
|
||||
# rice-power profile <name> power-saver | balanced | performance
|
||||
# rice-power lock|suspend|logout|reboot|shutdown
|
||||
# logout, reboot and shutdown ask first
|
||||
set -uo pipefail
|
||||
|
||||
run="${XDG_RUNTIME_DIR:-/tmp}/rice"
|
||||
mkdir -p "$run"
|
||||
|
||||
# CPU load since the previous call (the panel polls every few seconds).
|
||||
cpu_load() {
|
||||
local now prev idle total pidle ptotal
|
||||
read -r _ user nice system idle iowait irq softirq steal _ < /proc/stat
|
||||
total=$((user + nice + system + idle + iowait + irq + softirq + steal))
|
||||
idle=$((idle + iowait))
|
||||
if [[ -f $run/cpu.prev ]]; then
|
||||
read -r pidle ptotal < "$run/cpu.prev"
|
||||
(( total > ptotal )) && echo $(( 100 * ((total - ptotal) - (idle - pidle)) / (total - ptotal) )) || echo 0
|
||||
else
|
||||
echo 0
|
||||
fi
|
||||
echo "$idle $total" > "$run/cpu.prev"
|
||||
}
|
||||
|
||||
info() {
|
||||
local sensors gpu_dev profile profiles
|
||||
sensors=$(sensors -j 2>/dev/null || echo '{}')
|
||||
gpu_dev=$(ls -d /sys/class/drm/card*/device 2>/dev/null | while read -r d; do [[ -f $d/gpu_busy_percent ]] && echo "$d" && break; done)
|
||||
profile=$(powerprofilesctl get 2>/dev/null || echo "")
|
||||
profiles=$(powerprofilesctl list 2>/dev/null | sed -n 's/^[* ]*\([a-z-]*\):$/\1/p' | jq -R -s -c 'split("\n") | map(select(length > 0))')
|
||||
|
||||
jq -cn \
|
||||
--argjson sensors "$sensors" \
|
||||
--argjson cpu "$(cpu_load)" \
|
||||
--arg gpubusy "$( [[ -n $gpu_dev ]] && cat "$gpu_dev/gpu_busy_percent" 2>/dev/null)" \
|
||||
--arg vramused "$( [[ -n $gpu_dev ]] && cat "$gpu_dev/mem_info_vram_used" 2>/dev/null)" \
|
||||
--arg vramtotal "$( [[ -n $gpu_dev ]] && cat "$gpu_dev/mem_info_vram_total" 2>/dev/null)" \
|
||||
--arg mem "$(free -b | awk '/^Mem:/ { print $3 " " $2 }')" \
|
||||
--arg uptime "$(uptime -p | sed 's/^up //; s/ hours\?/h/; s/ minutes\?/m/; s/ days\?/d/; s/,//g')" \
|
||||
--arg profile "$profile" --argjson profiles "${profiles:-[]}" '
|
||||
def temp($pat): [$sensors | to_entries[] | select(.key | test($pat)) | .value | to_entries[]
|
||||
| select(.key | test("^(Tctl|Tdie|edge|Package id 0|temp1)$")) | .value
|
||||
| to_entries[] | select(.key | endswith("_input")) | .value] | first // null;
|
||||
def gib: (. / 1073741824 * 10 | round / 10 | tostring) + "G";
|
||||
($mem | split(" ") | map(tonumber)) as [$mused, $mtotal] |
|
||||
{
|
||||
cpu: {load: $cpu, temp: (temp("k10temp|coretemp|zenpower") | if . then round else null end)},
|
||||
gpu: {load: ($gpubusy | tonumber? // null),
|
||||
temp: (temp("amdgpu|radeon|nouveau") | if . then round else null end),
|
||||
vram: (if $vramtotal != "" then (($vramused | tonumber | gib) + " / " + ($vramtotal | tonumber | gib)) else "" end)},
|
||||
memory: {used: ($mused | gib), total: ($mtotal | gib), percent: ($mused * 100 / $mtotal | round)},
|
||||
uptime: $uptime,
|
||||
profile: $profile,
|
||||
profiles: [ $profiles[] | {name: ., label: ({"power-saver": "saver", "balanced": "balanced", "performance": "performance"}[.] // .), active: (. == $profile)} ]
|
||||
}'
|
||||
}
|
||||
|
||||
confirm() {
|
||||
local answer
|
||||
answer=$(printf '%s\nCancel\n' "$1" | rofi -dmenu -theme "$HOME/.config/rofi/menu.rasi" -p "$2" -mesg "Are you sure?") || return 1
|
||||
[[ $answer == "$1" ]]
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
info) info ;;
|
||||
profile)
|
||||
[[ ${2:-} =~ ^(power-saver|balanced|performance)$ ]] || { echo "rice-power: unknown profile '${2:-}'" >&2; exit 2; }
|
||||
powerprofilesctl set "$2" && rice-notify send "Power profile" "$2" -i power-profile-"$2"-symbolic
|
||||
;;
|
||||
lock) rice-panel close; rice-lock ;;
|
||||
suspend) rice-panel close; systemctl suspend ;;
|
||||
logout) rice-panel close; confirm " Log out" "Log out" && bspc quit ;;
|
||||
reboot) rice-panel close; confirm " Reboot" "Reboot" && systemctl reboot ;;
|
||||
shutdown) rice-panel close; confirm " Shut down" "Shut down" && systemctl poweroff ;;
|
||||
*) sed -n '2,8p' "$0" | sed 's/^# \{0,1\}//'; exit 2 ;;
|
||||
esac
|
||||
42
bin/rice-preview
Executable file
42
bin/rice-preview
Executable file
|
|
@ -0,0 +1,42 @@
|
|||
#!/usr/bin/env bash
|
||||
# rice-preview <file> - show a file in the terminal. Pictures and video frames
|
||||
# are drawn as real images (kitty's protocol in kitty, sixel in st), folders as
|
||||
# a tree, and everything else through bat. Used by `ff` (fzf) and `img`.
|
||||
set -uo pipefail
|
||||
|
||||
file="${1:?usage: rice-preview <file>}"
|
||||
[[ -e $file ]] || exit 0
|
||||
|
||||
cols="${FZF_PREVIEW_COLUMNS:-$(tput cols)}"
|
||||
lines="${FZF_PREVIEW_LINES:-$(( $(tput lines) - 2 ))}"
|
||||
cache="${XDG_CACHE_HOME:-$HOME/.cache}/rice/preview"
|
||||
|
||||
picture() {
|
||||
if [[ -n ${KITTY_WINDOW_ID:-} ]]; then
|
||||
kitty +kitten icat --clear --transfer-mode=memory --stdin=no --place="${cols}x${lines}@0x0" -- "$1"
|
||||
elif [[ $TERM == st* ]]; then
|
||||
chafa --format sixel --size "${cols}x${lines}" -- "$1"
|
||||
else
|
||||
chafa --format symbols --size "${cols}x${lines}" -- "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ -d $file ]]; then
|
||||
eza --tree --level=2 --icons=always --color=always -- "$file" 2>/dev/null || ls -la -- "$file"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
case "$(file -b --mime-type -- "$file")" in
|
||||
image/*)
|
||||
picture "$file"
|
||||
;;
|
||||
video/*)
|
||||
mkdir -p "$cache"
|
||||
frame="$cache/$(printf '%s:%s' "$(realpath -- "$file")" "$(stat -c %Y -- "$file")" | sha1sum | cut -c1-16).jpg"
|
||||
[[ -s $frame ]] || ffmpeg -loglevel error -y -ss 3 -i "$file" -frames:v 1 -vf scale=640:-1 "$frame" 2>/dev/null
|
||||
[[ -s $frame ]] && picture "$frame"
|
||||
;;
|
||||
*)
|
||||
bat --style=numbers --color=always --line-range :300 -- "$file" 2>/dev/null || head -n 300 -- "$file"
|
||||
;;
|
||||
esac
|
||||
107
bin/rice-reminder
Executable file
107
bin/rice-reminder
Executable file
|
|
@ -0,0 +1,107 @@
|
|||
#!/usr/bin/env bash
|
||||
# rice-reminder - desktop reminders backed by systemd user timers.
|
||||
#
|
||||
# rice-reminder new ask in rofi
|
||||
# rice-reminder add <when> <text> when: 20m | 1h30m | 15:30 | "tomorrow 9:00" | "2026-09-15 09:00"
|
||||
# rice-reminder list upcoming reminders as a notification
|
||||
# rice-reminder json upcoming reminders as JSON (calendar panel)
|
||||
# rice-reminder cancel <id> | clear
|
||||
#
|
||||
# In rofi, type "20m tea", "15:30 standup" or "tomorrow 9:00 | dentist".
|
||||
# Reminders use app name "rice", so they still show during do-not-disturb.
|
||||
set -uo pipefail
|
||||
|
||||
self="$(readlink -f "$0")"
|
||||
run="${XDG_RUNTIME_DIR:-/tmp}/rice"
|
||||
mkdir -p "$run"
|
||||
poke() { date +%s%N > "$run/reminder.rev"; }
|
||||
say() { notify-send -a rice -u low -i alarm-symbolic "$@"; }
|
||||
|
||||
# Resolve a "when" into epoch seconds, or fail.
|
||||
resolve() {
|
||||
local when="$1" now t
|
||||
now=$(date +%s)
|
||||
if [[ $when =~ ^([0-9]+[smhd])+$ ]]; then
|
||||
local secs=0 part
|
||||
for part in $(grep -oE '[0-9]+[smhd]' <<<"$when"); do
|
||||
case "${part: -1}" in
|
||||
s) secs=$((secs + ${part%?})) ;;
|
||||
m) secs=$((secs + ${part%?} * 60)) ;;
|
||||
h) secs=$((secs + ${part%?} * 3600)) ;;
|
||||
d) secs=$((secs + ${part%?} * 86400)) ;;
|
||||
esac
|
||||
done
|
||||
echo $((now + secs)); return
|
||||
fi
|
||||
if [[ $when =~ ^[0-9]{1,2}:[0-9]{2}$ ]]; then
|
||||
t=$(date -d "today $when" +%s 2>/dev/null) || return 1
|
||||
(( t <= now )) && t=$(date -d "tomorrow $when" +%s)
|
||||
echo "$t"; return
|
||||
fi
|
||||
t=$(date -d "$when" +%s 2>/dev/null) || return 1
|
||||
(( t > now )) || return 1
|
||||
echo "$t"
|
||||
}
|
||||
|
||||
add() {
|
||||
local when="$1" text="${2:-Reminder}" at unit
|
||||
at=$(resolve "$when") || { say "Reminder" "Couldn't read \"$when\" as a time"; return 1; }
|
||||
unit="rice-reminder-$(date +%s%N)"
|
||||
systemd-run --user --quiet --collect --unit="$unit" --description="$text" \
|
||||
--on-calendar="$(date -d "@$at" '+%Y-%m-%d %H:%M:%S')" \
|
||||
--timer-property=AccuracySec=1s \
|
||||
-- "$self" fire "$text" || { say "Reminder" "Could not schedule it"; return 1; }
|
||||
say "Reminder set" "$text · $(date -d "@$at" '+%a %H:%M')"
|
||||
poke
|
||||
}
|
||||
|
||||
upcoming() {
|
||||
local unit next
|
||||
systemctl --user list-timers 'rice-reminder-*' --all -o json 2>/dev/null |
|
||||
jq -r '.[] | [.unit, (.next / 1000000 | floor)] | @tsv' |
|
||||
while IFS=$'\t' read -r unit next; do
|
||||
(( next > 0 )) || continue
|
||||
printf '%s\t%s\t%s\n' "${unit%.timer}" "$next" "$(systemctl --user show -p Description --value "$unit")"
|
||||
done | sort -t$'\t' -k2,2n
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
new)
|
||||
input=$(rofi -dmenu -theme "$HOME/.config/rofi/prompt.rasi" -p "Reminder" \
|
||||
-mesg "20m tea · 15:30 standup · tomorrow 9:00 | dentist") || exit 0
|
||||
[[ -n $input ]] || exit 0
|
||||
if [[ $input == *"|"* ]]; then
|
||||
when="${input%%|*}"; text="${input#*|}"
|
||||
else
|
||||
when="${input%% *}"; text="${input#"$when"}"
|
||||
fi
|
||||
when="$(sed 's/^ *//; s/ *$//' <<<"$when")"; text="$(sed 's/^ *//; s/ *$//' <<<"$text")"
|
||||
add "$when" "${text:-Reminder}"
|
||||
;;
|
||||
add) add "${2:?when}" "${3:-Reminder}" ;;
|
||||
fire)
|
||||
notify-send -a rice -u critical -i alarm-symbolic "Reminder" "${2:-Reminder}"
|
||||
command -v paplay >/dev/null && [[ -f /usr/share/sounds/freedesktop/stereo/complete.oga ]] &&
|
||||
paplay /usr/share/sounds/freedesktop/stereo/complete.oga 2>/dev/null
|
||||
poke
|
||||
;;
|
||||
list)
|
||||
body=$(upcoming | while IFS=$'\t' read -r _ next text; do printf '%s %s\n' "$(date -d "@$next" '+%a %H:%M')" "$text"; done)
|
||||
say "Upcoming reminders" "${body:-Nothing scheduled}"
|
||||
;;
|
||||
json)
|
||||
upcoming | jq -R -s -c 'split("\n") | map(select(length > 0) | split("\t")
|
||||
| {id: .[0], at: (.[1] | tonumber), time: (.[1] | tonumber | strflocaltime("%H:%M")),
|
||||
date: (.[1] | tonumber | strflocaltime("%Y-%m-%d")), text: .[2]})'
|
||||
;;
|
||||
cancel)
|
||||
[[ ${2:-} =~ ^rice-reminder-[0-9]+$ ]] || { echo "rice-reminder: cancel takes a reminder id" >&2; exit 2; }
|
||||
systemctl --user stop "$2.timer" && poke
|
||||
;;
|
||||
clear)
|
||||
systemctl --user list-timers 'rice-reminder-*' --all --no-legend 2>/dev/null | awk '{ for (i = 1; i <= NF; i++) if ($i ~ /\.timer$/) print $i }' |
|
||||
xargs -r systemctl --user stop
|
||||
say "Reminders" "All cleared"; poke
|
||||
;;
|
||||
*) sed -n '2,12p' "$0" | sed 's/^# \{0,1\}//'; exit 2 ;;
|
||||
esac
|
||||
6
bin/rice-scratchpad
Executable file
6
bin/rice-scratchpad
Executable file
|
|
@ -0,0 +1,6 @@
|
|||
#!/usr/bin/env bash
|
||||
# rice-scratchpad - drop-down terminal on super + `. It is a persistent tmux
|
||||
# session, so whatever runs in it survives being hidden or closed.
|
||||
command -v tdrop >/dev/null || { rice-notify send "Drop-down terminal" "Install tdrop to use it"; exit 1; }
|
||||
exec tdrop -a -w 70% -h 45% -x 15% -y 45 --class rice-scratchpad \
|
||||
st -c rice-scratchpad -e tmux new-session -A -s scratchpad
|
||||
53
bin/rice-screensaver
Executable file
53
bin/rice-screensaver
Executable file
|
|
@ -0,0 +1,53 @@
|
|||
#!/usr/bin/env bash
|
||||
# rice-screensaver - classic xscreensaver hacks, full screen.
|
||||
#
|
||||
# rice-screensaver start [hack] random hack from the list (rice-idle runs this)
|
||||
# rice-screensaver stop
|
||||
# rice-screensaver now [hack] start by hand; press q or Escape in it to stop
|
||||
# rice-screensaver list hacks available on this machine
|
||||
#
|
||||
# Pick your own set by listing hack names, one per line, in
|
||||
# ~/.config/rice/screensavers.
|
||||
set -uo pipefail
|
||||
|
||||
hackdir=/usr/lib/xscreensaver
|
||||
run="${XDG_RUNTIME_DIR:-/tmp}/rice"
|
||||
pidfile="$run/screensaver.pid"
|
||||
mkdir -p "$run"
|
||||
|
||||
defaults="glmatrix flurry pipes fireworkx galaxy atlantis lament gears starfish xmatrix boxfit"
|
||||
user_list="${XDG_CONFIG_HOME:-$HOME/.config}/rice/screensavers"
|
||||
|
||||
available() {
|
||||
local names h
|
||||
if [[ -f $user_list ]]; then names=$(grep -v '^\s*#' "$user_list"); else names=$defaults; fi
|
||||
for h in $names; do [[ -x $hackdir/$h ]] && echo "$h"; done
|
||||
}
|
||||
|
||||
stop() {
|
||||
[[ -f $pidfile ]] && kill "$(cat "$pidfile")" 2>/dev/null
|
||||
rm -f "$pidfile"
|
||||
}
|
||||
|
||||
start() {
|
||||
local hack="${1:-}" pid wid
|
||||
stop
|
||||
if [[ -z $hack ]]; then hack=$(available | shuf -n1); fi
|
||||
[[ -n $hack && -x $hackdir/$hack ]] || { echo "rice-screensaver: no hack available (install xscreensaver)" >&2; exit 1; }
|
||||
[[ $hack =~ ^[a-z0-9_-]+$ ]] || exit 2
|
||||
|
||||
"$hackdir/$hack" >/dev/null 2>&1 &
|
||||
pid=$!
|
||||
echo "$pid" > "$pidfile"
|
||||
# Hacks open an ordinary window; hand it to bspwm as a fullscreen node.
|
||||
wid=$(xdotool search --sync --onlyvisible --pid "$pid" 2>/dev/null | head -n1)
|
||||
[[ -n $wid ]] && bspc node "$wid" -t fullscreen -f 2>/dev/null
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
start) start "${2:-}" ;;
|
||||
now) start "${2:-}" ;;
|
||||
stop) stop ;;
|
||||
list) available ;;
|
||||
*) sed -n '2,11p' "$0" | sed 's/^# \{0,1\}//'; exit 2 ;;
|
||||
esac
|
||||
81
bin/rice-session
Executable file
81
bin/rice-session
Executable file
|
|
@ -0,0 +1,81 @@
|
|||
#!/usr/bin/env bash
|
||||
# rice-session - start everything the desktop needs, exactly once.
|
||||
#
|
||||
# rice-session start called by bspwmrc (safe to call again on bspwm restart)
|
||||
# rice-session restart stop and start the rice daemons
|
||||
#
|
||||
# Output goes to ~/.local/state/rice/session.log.
|
||||
set -u
|
||||
|
||||
state="${XDG_STATE_HOME:-$HOME/.local/state}/rice"
|
||||
theme="$state/theme"
|
||||
mkdir -p "$state"
|
||||
exec >>"$state/session.log" 2>&1
|
||||
|
||||
running() { pgrep -u "$UID" -x "$1" >/dev/null; }
|
||||
|
||||
# start <process-name> <command...>: launch detached unless already running
|
||||
launch() {
|
||||
local name="$1"; shift
|
||||
if running "$name"; then return 0; fi
|
||||
command -v "$1" >/dev/null || { echo "missing: $1"; return 0; }
|
||||
setsid -f "$@" >/dev/null 2>&1 && echo "started $name"
|
||||
}
|
||||
|
||||
daemons=(xsettingsd picom sxhkd dunst greenclip flameshot eww polybar xidlehook xss-lock)
|
||||
|
||||
start() {
|
||||
echo "== $(date -Is) session start"
|
||||
systemctl --user import-environment DISPLAY XAUTHORITY XDG_CURRENT_DESKTOP PATH QT_QPA_PLATFORMTHEME GTK_THEME
|
||||
dbus-update-activation-environment --systemd DISPLAY XAUTHORITY XDG_CURRENT_DESKTOP
|
||||
|
||||
[[ -d $theme ]] || rice-theme render
|
||||
[[ -f $theme/Xresources ]] && xrdb -merge "$theme/Xresources"
|
||||
|
||||
# Daemons left over from another setup keep their old config files, so a
|
||||
# bspwm restart alone would never switch them. Replace those.
|
||||
local pid args
|
||||
for pid in $(pgrep -u "$UID" -x sxhkd); do
|
||||
args=$(tr '\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null)
|
||||
if [[ $args == *" -c "* && $args != *panel-keys* ]]; then kill "$pid" && echo "replaced: $args"; fi
|
||||
done
|
||||
for pid in $(pgrep -u "$UID" -x picom); do
|
||||
args=$(tr '\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null)
|
||||
if [[ $args != *"$HOME/.config/picom/picom.conf"* ]]; then kill "$pid" && echo "replaced: $args"; fi
|
||||
done
|
||||
for _ in {1..20}; do
|
||||
pgrep -u "$UID" -f 'sxhkd -c .*bspwm|picom --config .*bspwm' >/dev/null || break
|
||||
sleep 0.1
|
||||
done
|
||||
pgrep -u "$UID" -x dunst >/dev/null && dunstctl reload
|
||||
|
||||
launch xsettingsd xsettingsd -c "$theme/xsettingsd.conf"
|
||||
launch picom picom --config "$HOME/.config/picom/picom.conf"
|
||||
launch sxhkd sxhkd
|
||||
launch dunst dunst
|
||||
launch greenclip greenclip daemon
|
||||
launch flameshot flameshot
|
||||
launch eww eww daemon
|
||||
|
||||
if ! pgrep -u "$UID" -f polkit-gnome-authentication-agent-1 >/dev/null; then
|
||||
setsid -f /usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1 >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
rice-wallpaper restore
|
||||
rice-bar start
|
||||
rice-idle start
|
||||
|
||||
(sleep 2 && rice-hook post-boot) &
|
||||
}
|
||||
|
||||
stop() {
|
||||
echo "== $(date -Is) session stop"
|
||||
for d in "${daemons[@]}"; do pkill -u "$UID" -x "$d"; done
|
||||
}
|
||||
|
||||
case "${1:-start}" in
|
||||
start) start ;;
|
||||
stop) stop ;;
|
||||
restart) stop; sleep 0.5; start ;;
|
||||
*) echo "usage: rice-session start|stop|restart" >&2; exit 2 ;;
|
||||
esac
|
||||
102
bin/rice-status
Executable file
102
bin/rice-status
Executable file
|
|
@ -0,0 +1,102 @@
|
|||
#!/usr/bin/env bash
|
||||
# rice-status <module> - live text for polybar's rice modules (tail = true).
|
||||
# Modules: net bt vol notif indicators media. Each prints a line whenever its
|
||||
# state changes, in the same "LABEL value" style as the cpu/ram/gpu modules.
|
||||
set -uo pipefail
|
||||
|
||||
run="${XDG_RUNTIME_DIR:-/tmp}/rice"
|
||||
mkdir -p "$run"
|
||||
|
||||
accent=$(rice-theme get accent) dim=$(rice-theme get dim)
|
||||
urgent=$(rice-theme get urgent) green=$(rice-theme get green)
|
||||
yellow=$(rice-theme get yellow) magenta=$(rice-theme get magenta)
|
||||
|
||||
# label <colour> <LABEL> <value>
|
||||
label() { printf '%%{F%s}%s%%{F-} %s\n' "$1" "$2" "$3"; }
|
||||
short() { local s="$1" n="$2"; (( ${#s} > n )) && s="${s:0:n-1}…"; printf '%s' "$s"; }
|
||||
# wait for anything under $run to change, or give up after $1 seconds
|
||||
settle() { inotifywait -qq -t "$1" -e close_write,create,delete "$run" 2>/dev/null; return 0; }
|
||||
|
||||
net() {
|
||||
show() {
|
||||
local type conn
|
||||
IFS=: read -r type conn < <(nmcli -t -f TYPE,STATE,CONNECTION device status 2>/dev/null |
|
||||
awk -F: '$2 == "connected" && ($1 == "wifi" || $1 == "ethernet") { print $1 ":" $3; exit }')
|
||||
case "${type:-}" in
|
||||
wifi) label "$accent" NET "$(short "$conn" 16)" ;;
|
||||
ethernet) label "$accent" NET eth ;;
|
||||
*) label "$urgent" NET off ;;
|
||||
esac
|
||||
}
|
||||
show
|
||||
nmcli monitor 2>/dev/null | while read -r _; do show; done
|
||||
}
|
||||
|
||||
bt() {
|
||||
while :; do
|
||||
if ! bluetoothctl show 2>/dev/null | grep -q 'Powered: yes'; then
|
||||
label "$dim" BT off
|
||||
else
|
||||
local dev
|
||||
dev=$(bluetoothctl devices Connected 2>/dev/null | head -n1 | cut -d' ' -f3-)
|
||||
label "$accent" BT "$(short "${dev:-on}" 14)"
|
||||
fi
|
||||
settle 10
|
||||
done
|
||||
}
|
||||
|
||||
vol() {
|
||||
show() {
|
||||
local v m
|
||||
read -r _ v m < <(wpctl get-volume @DEFAULT_AUDIO_SINK@ 2>/dev/null)
|
||||
if [[ -n ${m:-} ]]; then label "$urgent" VOL muted
|
||||
else label "$green" VOL "$(awk -v v="${v:-0}" 'BEGIN { printf "%d%%", v * 100 + 0.5 }')"; fi
|
||||
}
|
||||
show
|
||||
pactl subscribe 2>/dev/null | grep --line-buffered -E "'change' on (sink|server)" | while read -r _; do show; done
|
||||
}
|
||||
|
||||
notif() {
|
||||
while :; do
|
||||
local n level
|
||||
n=$(dunstctl count history 2>/dev/null || echo 0)
|
||||
level=$(dunstctl get-pause-level 2>/dev/null || echo 0)
|
||||
if (( level > 0 )); then printf '%%{F%s}%%{F-}\n' "$yellow"
|
||||
elif (( n > 0 )); then printf '%%{F%s}%%{F-} %s\n' "$accent" "$n"
|
||||
else printf '%%{F%s}%%{F-}\n' "$dim"; fi
|
||||
settle 5
|
||||
done
|
||||
}
|
||||
|
||||
indicators() {
|
||||
while :; do
|
||||
local out="" r
|
||||
[[ -f $run/stay-awake ]] && out+="%{A1:rice-idle toggle:}%{F$yellow}%{F-}%{A} "
|
||||
pgrep -x gpu-screen-reco >/dev/null && out+="%{A1:rice-capture record:}%{F$urgent} REC%{F-}%{A} "
|
||||
pgrep -f 'nerd-dictation begin' >/dev/null && out+="%{A1:rice-dictate toggle:}%{F$magenta}%{F-}%{A} "
|
||||
r=$(systemctl --user list-timers 'rice-reminder-*' --no-legend 2>/dev/null | grep -c .)
|
||||
(( r > 0 )) && out+="%{A1:rice-reminder list:}%{F$accent} $r%{F-}%{A} "
|
||||
printf '%s\n' "${out% }"
|
||||
settle 5
|
||||
done
|
||||
}
|
||||
|
||||
media() {
|
||||
while :; do
|
||||
echo ""
|
||||
playerctl --follow metadata --format '{{status}}|{{artist}}|{{title}}' 2>/dev/null |
|
||||
while IFS='|' read -r status artist title; do
|
||||
case "$status" in
|
||||
Playing) printf '%%{F%s}%%{F-} %s\n' "$magenta" "$(short "${artist:+$artist - }$title" 40)" ;;
|
||||
Paused) printf '%%{F%s} %s%%{F-}\n' "$dim" "$(short "$title" 30)" ;;
|
||||
*) echo "" ;;
|
||||
esac
|
||||
done
|
||||
sleep 2
|
||||
done
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
net|bt|vol|notif|indicators|media) "$1" ;;
|
||||
*) echo "usage: rice-status net|bt|vol|notif|indicators|media" >&2; exit 2 ;;
|
||||
esac
|
||||
162
bin/rice-sysinfo
Executable file
162
bin/rice-sysinfo
Executable file
|
|
@ -0,0 +1,162 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# Default values
|
||||
MODE=""
|
||||
NORMAL_COLOR=""
|
||||
CRITICAL_COLOR=""
|
||||
CRITICAL_THRESHOLD=80
|
||||
CLICK_ACTION=false
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--cpu)
|
||||
MODE="cpu"
|
||||
shift
|
||||
;;
|
||||
--ram)
|
||||
MODE="ram"
|
||||
shift
|
||||
;;
|
||||
--gpu)
|
||||
MODE="gpu"
|
||||
shift
|
||||
;;
|
||||
--normal-color)
|
||||
NORMAL_COLOR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--critical-color)
|
||||
CRITICAL_COLOR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--click)
|
||||
CLICK_ACTION=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Function to get CPU usage
|
||||
get_cpu_usage() {
|
||||
# Using top to get CPU usage
|
||||
local cpu_usage=$(top -bn2 -d 0.5 | grep "Cpu(s)" | tail -n1 | awk '{print $2}' | cut -d'%' -f1)
|
||||
|
||||
# If top doesn't work, try mpstat
|
||||
if [ -z "$cpu_usage" ]; then
|
||||
if command -v mpstat >/dev/null 2>&1; then
|
||||
cpu_usage=$(mpstat 1 1 | awk '/Average:/ {print 100 - $NF}')
|
||||
fi
|
||||
fi
|
||||
|
||||
# Round to integer
|
||||
printf "%.0f" "$cpu_usage"
|
||||
}
|
||||
|
||||
# Function to get RAM usage
|
||||
get_ram_usage() {
|
||||
local mem_info=$(free | grep Mem)
|
||||
local total=$(echo "$mem_info" | awk '{print $2}')
|
||||
local used=$(echo "$mem_info" | awk '{print $3}')
|
||||
local percentage=$(awk "BEGIN {printf \"%.0f\", ($used/$total)*100}")
|
||||
echo "$percentage"
|
||||
}
|
||||
|
||||
# Function to get GPU usage (NVIDIA)
|
||||
get_gpu_usage() {
|
||||
# amdgpu (and i915 on newer kernels) expose load directly; reading it is
|
||||
# free, unlike radeontop, which spawns a sampling process on every tick.
|
||||
local busy
|
||||
busy=$(cat /sys/class/drm/card*/device/gpu_busy_percent 2>/dev/null | head -n1)
|
||||
if [[ -n $busy ]]; then
|
||||
echo "$busy"
|
||||
elif command -v nvidia-smi >/dev/null 2>&1; then
|
||||
local gpu_usage=$(nvidia-smi --query-gpu=utilization.gpu --format=csv,noheader,nounits | head -n1)
|
||||
echo "$gpu_usage"
|
||||
elif command -v intel_gpu_top >/dev/null 2>&1; then
|
||||
# For Intel GPUs - approximate method
|
||||
echo "0"
|
||||
else
|
||||
echo "N/A"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to format output with color
|
||||
format_output() {
|
||||
local value="$1"
|
||||
local normal_color="$2"
|
||||
local critical_color="$3"
|
||||
|
||||
# Check if value is a number
|
||||
if [[ "$value" =~ ^[0-9]+$ ]]; then
|
||||
if [ "$value" -ge "$CRITICAL_THRESHOLD" ] && [ -n "$critical_color" ]; then
|
||||
echo "%{F${critical_color}}${value}%%{F-}"
|
||||
elif [ -n "$normal_color" ]; then
|
||||
echo "%{F${normal_color}}${value}%%{F-}"
|
||||
else
|
||||
echo "${value}%"
|
||||
fi
|
||||
else
|
||||
# Not a number (like N/A)
|
||||
echo "$value"
|
||||
fi
|
||||
}
|
||||
|
||||
# Handle click actions
|
||||
handle_click() {
|
||||
case $MODE in
|
||||
cpu)
|
||||
# Open system monitor or htop
|
||||
if command -v htop >/dev/null 2>&1; then
|
||||
alacritty -e htop &
|
||||
elif command -v gnome-system-monitor >/dev/null 2>&1; then
|
||||
gnome-system-monitor &
|
||||
elif command -v xfce4-taskmanager >/dev/null 2>&1; then
|
||||
xfce4-taskmanager &
|
||||
else
|
||||
notify-send "System Info" "No system monitor found"
|
||||
fi
|
||||
;;
|
||||
gpu)
|
||||
# Open nvidia-smi or GPU monitor
|
||||
if command -v nvidia-smi >/dev/null 2>&1; then
|
||||
alacritty -e watch -n 1 nvidia-smi &
|
||||
else
|
||||
notify-send "System Info" "No GPU monitor available"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Main logic
|
||||
main() {
|
||||
if [ "$CLICK_ACTION" = true ]; then
|
||||
handle_click
|
||||
exit 0
|
||||
fi
|
||||
|
||||
case $MODE in
|
||||
cpu)
|
||||
usage=$(get_cpu_usage)
|
||||
format_output "$usage" "$NORMAL_COLOR" "$CRITICAL_COLOR"
|
||||
;;
|
||||
ram)
|
||||
usage=$(get_ram_usage)
|
||||
format_output "$usage" "$NORMAL_COLOR" "$CRITICAL_COLOR"
|
||||
;;
|
||||
gpu)
|
||||
usage=$(get_gpu_usage)
|
||||
format_output "$usage" "$NORMAL_COLOR" "$CRITICAL_COLOR"
|
||||
;;
|
||||
*)
|
||||
echo "Error: Specify --cpu, --ram, or --gpu"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
main
|
||||
170
bin/rice-theme
Executable file
170
bin/rice-theme
Executable file
|
|
@ -0,0 +1,170 @@
|
|||
#!/usr/bin/env bash
|
||||
# rice-theme - render and apply a colour theme to every surface of the rice.
|
||||
#
|
||||
# rice-theme set <name> render themes/<name> and reload everything
|
||||
# rice-theme reload re-render the current theme (after editing a template)
|
||||
# rice-theme list available themes
|
||||
# rice-theme current name of the active theme
|
||||
# rice-theme get <key> one value from the active colors.toml (for scripts)
|
||||
#
|
||||
# Templates live in $RICE_DIR/templates/*.tpl and ~/.config/rice/templates/*.tpl
|
||||
# (yours win). Placeholders, for a key like accent = "#92b9ea":
|
||||
# {{ accent }} #92b9ea
|
||||
# {{ accent_strip }} 92b9ea
|
||||
# {{ accent_rgb }} 146,185,234
|
||||
# A theme can ship a finished file (e.g. themes/foo/btop.theme); it is used
|
||||
# as-is instead of the template with the same output name.
|
||||
set -euo pipefail
|
||||
|
||||
RICE_DIR="${RICE_DIR:-$(cd "$(dirname "$(readlink -f "$0")")/.." && pwd)}"
|
||||
STATE="${XDG_STATE_HOME:-$HOME/.local/state}/rice"
|
||||
CURRENT="$STATE/theme"
|
||||
USER_TEMPLATES="${XDG_CONFIG_HOME:-$HOME/.config}/rice/templates"
|
||||
HOOKS="${XDG_CONFIG_HOME:-$HOME/.config}/rice/hooks/theme-set.d"
|
||||
OVERRIDE="${XDG_CONFIG_HOME:-$HOME/.config}/rice/theme.override.toml"
|
||||
|
||||
die() { printf 'rice-theme: %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
theme_dir() {
|
||||
local user="${XDG_CONFIG_HOME:-$HOME/.config}/rice/themes/$1"
|
||||
if [[ -d $user ]]; then echo "$user"
|
||||
elif [[ -d $RICE_DIR/themes/$1 ]]; then echo "$RICE_DIR/themes/$1"
|
||||
else return 1; fi
|
||||
}
|
||||
|
||||
# Emit "key<TAB>value" for every key = value line of a colors.toml.
|
||||
# Quoted values are taken whole (a colour's # is not a comment); bare values
|
||||
# stop at a trailing # comment.
|
||||
parse_colors() {
|
||||
sed -n -E \
|
||||
-e 's/^[[:space:]]*([a-z_][a-z0-9_]*)[[:space:]]*=[[:space:]]*"([^"]*)".*$/\1\t\2/p' \
|
||||
-e 't' \
|
||||
-e 's/^[[:space:]]*([a-z_][a-z0-9_]*)[[:space:]]*=[[:space:]]*([^"#[:space:]]([^#]*[^#[:space:]])?)[[:space:]]*(#.*)?$/\1\t\2/p' \
|
||||
"$1"
|
||||
}
|
||||
|
||||
# Build a sed program that replaces every placeholder form for every key.
|
||||
sed_program() {
|
||||
local key value esc r g b
|
||||
while IFS=$'\t' read -r key value; do
|
||||
esc=$(printf '%s' "$value" | sed -e 's/[\\&|]/\\&/g')
|
||||
printf 's|{{ *%s *}}|%s|g\n' "$key" "$esc"
|
||||
if [[ $value =~ ^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$ ]]; then
|
||||
r=$((16#${BASH_REMATCH[1]})); g=$((16#${BASH_REMATCH[2]})); b=$((16#${BASH_REMATCH[3]}))
|
||||
printf 's|{{ *%s_strip *}}|%s|g\n' "$key" "${value#\#}"
|
||||
printf 's|{{ *%s_rgb *}}|%s,%s,%s|g\n' "$key" "$r" "$g" "$b"
|
||||
fi
|
||||
done < <(parse_colors "$1" | tac | awk -F'\t' '!seen[$1]++') # a later line wins (overrides)
|
||||
}
|
||||
|
||||
render() {
|
||||
local name="$1" src out next prog tpl base
|
||||
src=$(theme_dir "$name") || die "no theme called '$name' (try: rice-theme list)"
|
||||
[[ -f $src/colors.toml ]] || die "$src has no colors.toml"
|
||||
|
||||
mkdir -p "$STATE"
|
||||
next=$(mktemp -d "$STATE/.theme.XXXXXX")
|
||||
cp -a "$src/." "$next/"
|
||||
# Your overrides (the font from rice-font, or any colour) apply on top of every theme.
|
||||
if [[ -f $OVERRIDE ]]; then
|
||||
printf '\n# --- from %s ---\n' "$OVERRIDE" >> "$next/colors.toml"
|
||||
cat "$OVERRIDE" >> "$next/colors.toml"
|
||||
fi
|
||||
prog="$next/.render.sed"
|
||||
sed_program "$next/colors.toml" > "$prog"
|
||||
|
||||
# User templates first, so a same-named built-in is skipped.
|
||||
for tpl in "$USER_TEMPLATES"/*.tpl "$RICE_DIR"/templates/*.tpl; do
|
||||
[[ -f $tpl ]] || continue
|
||||
base=$(basename "$tpl" .tpl)
|
||||
out="$next/$base"
|
||||
[[ -e $out ]] && continue
|
||||
sed -f "$prog" "$tpl" > "$out"
|
||||
if grep -q '{{' "$out"; then
|
||||
printf 'rice-theme: %s has unknown placeholders: %s\n' "$base" \
|
||||
"$(grep -o '{{[^}]*}}' "$out" | sort -u | tr '\n' ' ')" >&2
|
||||
fi
|
||||
done
|
||||
rm -f "$prog"
|
||||
|
||||
rm -rf "$CURRENT.old"
|
||||
[[ -d $CURRENT ]] && mv "$CURRENT" "$CURRENT.old"
|
||||
mv "$next" "$CURRENT"
|
||||
rm -rf "$CURRENT.old"
|
||||
printf '%s\n' "$name" > "$STATE/theme.name"
|
||||
}
|
||||
|
||||
# signal_if_caught <SIGNAL> <process-name>: send the signal only to processes
|
||||
# whose SigCgt mask says they install a handler for it.
|
||||
signal_if_caught() {
|
||||
local sig="$1" name="$2" num pid mask
|
||||
num=$(kill -l "$sig")
|
||||
for pid in $(pgrep -u "$UID" -x "$name"); do
|
||||
mask=$(awk '/^SigCgt:/ { print $2 }' "/proc/$pid/status" 2>/dev/null)
|
||||
[[ -n $mask ]] && (( (16#$mask >> (num - 1)) & 1 )) && kill -"$sig" "$pid"
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
# Push the freshly rendered files into running programs. Each step is
|
||||
# best-effort: a program that isn't running simply has nothing to reload.
|
||||
apply() {
|
||||
local t="$CURRENT"
|
||||
[[ -f $t/bspwm-colors.sh ]] && pgrep -x bspwm >/dev/null && bash "$t/bspwm-colors.sh"
|
||||
[[ -f $t/Xresources ]] && command -v xrdb >/dev/null && xrdb -merge "$t/Xresources"
|
||||
# One cursor for X, GTK and Qt apps: the default cursor theme follows the rice's.
|
||||
local cursor; cursor=$("$0" get cursor_theme 2>/dev/null)
|
||||
if [[ -n $cursor ]]; then
|
||||
mkdir -p "$HOME/.icons/default"
|
||||
printf '[Icon Theme]\nInherits=%s\n' "$cursor" > "$HOME/.icons/default/index.theme"
|
||||
fi
|
||||
pgrep -x polybar >/dev/null && polybar-msg cmd restart >/dev/null 2>&1 || true
|
||||
pgrep -x dunst >/dev/null && dunstctl reload >/dev/null 2>&1 || true
|
||||
pgrep -x eww >/dev/null && eww reload >/dev/null 2>&1 || true
|
||||
# USR1 makes kitty and the rice's st re-read their colours, but it kills a
|
||||
# process that doesn't handle it (an older st build), so check first.
|
||||
signal_if_caught USR1 kitty
|
||||
signal_if_caught USR1 st
|
||||
pgrep -x xsettingsd >/dev/null && pkill -HUP -x xsettingsd || true
|
||||
if command -v flameshot >/dev/null; then
|
||||
flameshot config --maincolor "$("$0" get accent)" --contrastcolor "$("$0" get bg)" \
|
||||
--showhelp false --trayicon false >/dev/null 2>&1 || true
|
||||
fi
|
||||
if [[ -d $HOOKS ]]; then
|
||||
for h in "$HOOKS"/*; do [[ -x $h ]] && "$h" "$(cat "$STATE/theme.name")" || true; done
|
||||
fi
|
||||
}
|
||||
|
||||
cmd="${1:-}"
|
||||
case "$cmd" in
|
||||
set)
|
||||
[[ -n ${2:-} ]] || die "usage: rice-theme set <name>"
|
||||
exec 9>"${XDG_RUNTIME_DIR:-/tmp}/rice-theme.lock"; flock 9
|
||||
render "$2"; apply
|
||||
# Only a theme change swaps the wallpaper; `reload` keeps the one you picked.
|
||||
command -v rice-wallpaper >/dev/null && { rice-wallpaper theme-default >/dev/null 2>&1 || true; }
|
||||
command -v notify-send >/dev/null && notify-send -a rice -u low "Theme" "$(grep -m1 '^name' "$CURRENT/colors.toml" | cut -d'"' -f2)" || true
|
||||
;;
|
||||
render)
|
||||
# Render only, touch no running program (used by install.sh and for testing).
|
||||
render "${2:-$(cat "$STATE/theme.name" 2>/dev/null || echo sakura-line)}"
|
||||
;;
|
||||
reload)
|
||||
exec 9>"${XDG_RUNTIME_DIR:-/tmp}/rice-theme.lock"; flock 9
|
||||
render "$(cat "$STATE/theme.name" 2>/dev/null || echo sakura-line)"; apply
|
||||
;;
|
||||
list)
|
||||
{ ls -1 "$RICE_DIR/themes"; ls -1 "${XDG_CONFIG_HOME:-$HOME/.config}/rice/themes" 2>/dev/null; } | sort -u
|
||||
;;
|
||||
current)
|
||||
cat "$STATE/theme.name" 2>/dev/null || echo sakura-line
|
||||
;;
|
||||
get)
|
||||
[[ -n ${2:-} ]] || die "usage: rice-theme get <key>"
|
||||
parse_colors "$CURRENT/colors.toml" | awk -F'\t' -v k="$2" '$1 == k { v = $2; found = 1 } END { if (found) print v; exit !found }'
|
||||
;;
|
||||
*)
|
||||
sed -n '2,15p' "$0" | sed 's/^# \{0,1\}//'
|
||||
[[ -z $cmd ]] || exit 2
|
||||
;;
|
||||
esac
|
||||
28
bin/rice-volume
Executable file
28
bin/rice-volume
Executable file
|
|
@ -0,0 +1,28 @@
|
|||
#!/usr/bin/env bash
|
||||
# rice-volume up|down|mute|mic-mute|set <percent> - volume with an on-screen display.
|
||||
set -uo pipefail
|
||||
|
||||
sink=@DEFAULT_AUDIO_SINK@
|
||||
src=@DEFAULT_AUDIO_SOURCE@
|
||||
step=5
|
||||
|
||||
# prints "<percent> <muted|on>"
|
||||
level() { wpctl get-volume "$1" | awk '{ printf "%d %s\n", $2 * 100 + 0.5, ($3 == "[MUTED]") ? "muted" : "on" }'; }
|
||||
|
||||
case "${1:-}" in
|
||||
up) wpctl set-mute "$sink" 0; wpctl set-volume -l 1.0 "$sink" "${step}%+" ;;
|
||||
down) wpctl set-volume "$sink" "${step}%-" ;;
|
||||
mute) wpctl set-mute "$sink" toggle ;;
|
||||
# set comes from the panel slider, which already shows the level: no OSD.
|
||||
set) wpctl set-volume -l 1.0 "$sink" "${2:?usage: rice-volume set <percent>}%"; exit 0 ;;
|
||||
mic-mute)
|
||||
wpctl set-mute "$src" toggle
|
||||
read -r v s < <(level "$src")
|
||||
if [[ $s == muted ]]; then rice-osd Microphone muted; else rice-osd Microphone "$v"; fi
|
||||
exit 0
|
||||
;;
|
||||
*) echo "usage: rice-volume up|down|mute|mic-mute|set <percent>" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
read -r v s < <(level "$sink")
|
||||
if [[ $s == muted ]]; then rice-osd Volume muted; else rice-osd Volume "$v"; fi
|
||||
56
bin/rice-wallpaper
Executable file
56
bin/rice-wallpaper
Executable file
|
|
@ -0,0 +1,56 @@
|
|||
#!/usr/bin/env bash
|
||||
# rice-wallpaper - the desktop background.
|
||||
#
|
||||
# rice-wallpaper set <image> set it and remember it
|
||||
# rice-wallpaper restore set the remembered one (session start)
|
||||
# rice-wallpaper theme-default set the active theme's own wallpaper
|
||||
# rice-wallpaper next the next image in the list
|
||||
# rice-wallpaper list images from the rice, ~/.config/rice/wallpapers and ~/Pictures/Wallpapers
|
||||
# rice-wallpaper thumb <image> a cached 400x250 thumbnail for the pickers
|
||||
set -uo pipefail
|
||||
|
||||
RICE_DIR="${RICE_DIR:-$(cd "$(dirname "$(readlink -f "$0")")/.." && pwd)}"
|
||||
state="${XDG_STATE_HOME:-$HOME/.local/state}/rice"
|
||||
remembered="$state/wallpaper"
|
||||
thumbs="${XDG_CACHE_HOME:-$HOME/.cache}/rice/thumbs"
|
||||
mkdir -p "$state" "$thumbs"
|
||||
|
||||
list() {
|
||||
find -L "$RICE_DIR/wallpapers" "${XDG_CONFIG_HOME:-$HOME/.config}/rice/wallpapers" "$HOME/Pictures/Wallpapers" \
|
||||
-maxdepth 1 -type f \( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.webp' \) 2>/dev/null | sort
|
||||
}
|
||||
|
||||
apply() {
|
||||
[[ -f $1 ]] || { echo "rice-wallpaper: no such image: $1" >&2; return 1; }
|
||||
feh --no-fehbg --bg-fill "$1" && realpath -- "$1" > "$remembered"
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
set) apply "${2:?usage: rice-wallpaper set <image>}" ;;
|
||||
restore)
|
||||
if [[ -f $remembered && -f $(cat "$remembered") ]]; then apply "$(cat "$remembered")"; else "$0" theme-default; fi
|
||||
;;
|
||||
theme-default)
|
||||
name=$(rice-theme get wallpaper 2>/dev/null)
|
||||
[[ -n $name && -f $RICE_DIR/wallpapers/$name ]] && apply "$RICE_DIR/wallpapers/$name"
|
||||
;;
|
||||
next)
|
||||
mapfile -t all < <(list)
|
||||
(( ${#all[@]} > 0 )) || exit 1
|
||||
current=$(cat "$remembered" 2>/dev/null)
|
||||
for i in "${!all[@]}"; do
|
||||
if [[ ${all[$i]} == "$current" ]]; then apply "${all[$(( (i + 1) % ${#all[@]} ))]}"; exit; fi
|
||||
done
|
||||
apply "${all[0]}"
|
||||
;;
|
||||
list) list ;;
|
||||
thumb)
|
||||
img="${2:?usage: rice-wallpaper thumb <image>}"
|
||||
out="$thumbs/$(printf '%s:%s' "$img" "$(stat -c %Y -- "$img" 2>/dev/null)" | sha1sum | cut -c1-16).jpg"
|
||||
if [[ ! -s $out ]] && command -v magick >/dev/null; then
|
||||
magick "${img}[0]" -thumbnail '400x250^' -gravity center -extent 400x250 -quality 85 "$out" 2>/dev/null
|
||||
fi
|
||||
if [[ -s $out ]]; then echo "$out"; else echo "$img"; fi
|
||||
;;
|
||||
*) sed -n '2,10p' "$0" | sed 's/^# \{0,1\}//'; exit 2 ;;
|
||||
esac
|
||||
5
bin/rice-zoom
Executable file
5
bin/rice-zoom
Executable file
|
|
@ -0,0 +1,5 @@
|
|||
#!/usr/bin/env bash
|
||||
# rice-zoom - zoom into the screen. Scroll to zoom, drag to pan, q or Escape to leave.
|
||||
command -v boomer >/dev/null || { rice-notify send "Zoom" "Install boomer-git to zoom"; exit 1; }
|
||||
pgrep -x boomer >/dev/null && exit 0
|
||||
exec boomer
|
||||
Loading…
Add table
Add a link
Reference in a new issue