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.
This commit is contained in:
Sarthak 2026-09-14 16:12:09 +05:30
commit 7f41cf7a82
104 changed files with 8077 additions and 0 deletions

9
.gitignore vendored Normal file
View file

@ -0,0 +1,9 @@
# build output
particles/particles
particles/*.o
# never commit private files, even if someone copies them in here
home/.config/rice/secrets/
home/.config/rice/local.sh
home/.bashrc.local
home/.config/rice/theme.override.toml

106
README.md Normal file
View file

@ -0,0 +1,106 @@
# Sakura Line
A bspwm rice for Arch Linux where everything - bar, menus, popups,
notifications, lock screen, terminal and prompt - is drawn from one palette and
one set of shapes, so it feels like one system instead of a pile of tools.
![Sakura Line](docs/old-screenshot.webp)
## Install
On a clean Arch install, as your normal user:
```sh
curl -fsSL https://rice.srtk.in/install.sh | bash
```
or from a clone:
```sh
git clone https://git.srtk.in/sarthak/dotfiles.git ~/rice
~/rice/install.sh # full install, safe to re-run
~/rice/install.sh --dry-run # print every step, change nothing
~/rice/install.sh --update # pull, re-link, re-render, rebuild what changed
```
The installer installs the packages (bootstrapping paru for the AUR), symlinks
every file in `home/` into `$HOME` and every script in `bin/` into
`~/.local/bin`, renders the theme, builds st with sixel images, and offers
calendar sync and a stock Plymouth boot splash. Anything it replaces is moved
to `~/.rice-backup-<date>/`. Log in on tty1 and the desktop starts.
## What you get
| | |
|---|---|
| Window manager | bspwm + sxhkd |
| Bar | polybar, floating and rounded; every label opens its panel |
| Popups | eww: control centre, audio, network, bluetooth, display, power, media, calendar, notifications |
| Menus | rofi: launcher, rice menu, power, toggles, capture, theme and wallpaper pickers, emoji, clipboard, keybindings |
| Notifications | dunst, with a notification centre, per-notification dismiss and do-not-disturb |
| Compositor | picom 13, one motion language for windows and popups |
| Terminal | st (st-flexipatch with sixel, ligatures, scrollback) and kitty |
| Prompt | starship, one line: `user@host dir branch ` |
| Idle | xscreensaver hacks after 5 minutes, xsecurelock after 10, skipped during fullscreen video |
| Calendar | khal + vdirsyncer against any CalDAV server; reminders as systemd timers |
| Capture | flameshot, gpu-screen-recorder, OCR, QR codes, colour picker, webcam overlay, transcode |
`super + k` lists every shortcut. The ones to learn first:
| | |
|---|---|
| `super + Return` | terminal |
| `super + w` / `super + m` | app launcher / rice menu |
| `super + a` / `super + n` | control centre / notification centre |
| `super + ctrl + a / w / b / d / p / m` | audio / network / bluetooth / display / power / media |
| `super + ctrl + c` | calendar |
| `super + ctrl + r` | new reminder ("20m tea", "15:30 standup") |
| `super + c` / `super + v` | copy / paste, terminals included |
| `super + ctrl + v` / `super + ctrl + e` | clipboard history / emoji |
| `Print` or `super + alt + p` | screenshot a region |
| `shift + Print` or `super + alt + shift + p` | screenshot the whole screen |
| `ctrl + Print` or `super + alt + t` | copy text from a region |
| `alt + Print` or `super + alt + v` | start or stop recording |
| `super + shift + Print` or `super + alt + c` | pick a colour |
| `super + Print` or `super + alt + s` | capture menu |
| `` super + ` `` | drop-down terminal |
| `super + Escape` / `super + ctrl + l` | power menu / lock |
## Theming
A theme is one file, `themes/<name>/colors.toml`. `rice-theme set <name>`
renders every file in `templates/` from it into `~/.local/state/rice/theme/`
and reloads the running apps; configs include or link those rendered files.
- Your own themes go in `~/.config/rice/themes/<name>/`.
- `~/.config/rice/theme.override.toml` wins over every theme (`rice-font` writes the font there).
- Your own templates go in `~/.config/rice/templates/*.tpl` and win over the built-in ones.
Placeholders are `{{ key }}`, `{{ key_strip }}` (no `#`) and `{{ key_rgb }}` (`r,g,b`).
## Your own settings
These are never in the repo and never overwritten:
| | |
|---|---|
| `~/.bashrc.local` | personal aliases, exports, SDK paths |
| `~/.config/rice/local.sh` | autostarted apps, per-app desktops, monitor tweaks (sourced by bspwmrc) |
| `~/.config/rice/idle.conf` | `SCREENSAVER=` and `LOCK=` in seconds |
| `~/.config/rice/screensavers` | the xscreensaver hacks to rotate through |
| `~/.config/rice/hooks/<event>.d/` | scripts run on `post-boot`, `theme-set`, `font-set`, `wallpaper-set`, `lock`, `unlock`, `update` |
| `~/.config/rice/secrets/` | the CalDAV password (mode 600) |
## Layout
```
install.sh one-shot installer
packages/ pacman.txt and aur.txt
bin/ rice-* scripts: panels' data and actions, menus, capture, idle, theme
home/ files linked into $HOME
templates/ *.tpl rendered from a theme's colors.toml
themes/ one directory per theme
st/ st-flexipatch patch list, config changes and build script
particles/ the particle wallpaper layer
wallpapers/
```

3
bin/rice-about Executable file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

BIN
docs/old-screenshot.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

16
home/.bash_profile Normal file
View file

@ -0,0 +1,16 @@
# ~/.bash_profile
[[ -f ~/.bashrc ]] && . ~/.bashrc
# Auto-start the graphical session, but ONLY in the exact situation where it is
# safe to do so. Any one of these guards failing leaves you at a normal shell.
# - not already inside X
# - not an SSH session (never hijack a remote login)
# - physically on VT 1
# - no Xorg already running for this user
# NOTE: deliberately NOT `exec startx`. If X dies or fails to start we fall back
# to an interactive shell on tty1 instead of a login loop.
if [[ -z "$DISPLAY" && -z "$SSH_CONNECTION" && "$XDG_VTNR" == "1" ]]; then
if ! pgrep -u "$USER" -x Xorg >/dev/null 2>&1; then
startx
fi
fi

47
home/.bashrc Normal file
View file

@ -0,0 +1,47 @@
# Sakura Line - bash.
# Aliases and functions live in ~/.config/rice/bash/. Anything personal or
# machine-only (work aliases, SDK paths, tokens) goes in ~/.bashrc.local,
# which is not part of the repo.
export PATH="$HOME/.local/bin:$PATH"
[[ $- != *i* ]] && { [[ -f $HOME/.bashrc.local ]] && . "$HOME/.bashrc.local"; return; }
export EDITOR=nvim
export VISUAL=nvim
export BAT_THEME=ansi
export MANPAGER="sh -c 'col -bx | bat -l man -p'"
export MANROFFOPT="-c"
theme="${XDG_STATE_HOME:-$HOME/.local/state}/rice/theme"
# --- history ---
HISTSIZE=50000
HISTFILESIZE=50000
HISTCONTROL=ignoreboth:erasedups
shopt -s histappend checkwinsize globstar
PROMPT_COMMAND="history -a${PROMPT_COMMAND:+; $PROMPT_COMMAND}"
# --- completion ---
[[ -r /usr/share/bash-completion/bash_completion ]] && . /usr/share/bash-completion/bash_completion
# --- prompt: two lines in the theme's colours ---
if command -v starship >/dev/null; then
export STARSHIP_CONFIG="$theme/starship.toml"
eval "$(starship init bash)"
fi
# --- fzf: Ctrl+R history, Ctrl+T files, Alt+C directories, coloured like rofi ---
[[ -f $theme/fzf.sh ]] && . "$theme/fzf.sh"
[[ -r /usr/share/fzf/key-bindings.bash ]] && . /usr/share/fzf/key-bindings.bash
[[ -r /usr/share/fzf/completion.bash ]] && . /usr/share/fzf/completion.bash
# --- zoxide: `z` jumps to directories you have been in before ---
command -v zoxide >/dev/null && eval "$(zoxide init bash)"
# --- rice aliases and functions ---
for rc in "$HOME"/.config/rice/bash/*.bash; do
[[ -r $rc ]] && . "$rc"
done
unset rc
[[ -f $HOME/.bashrc.local ]] && . "$HOME/.bashrc.local"

68
home/.config/bspwm/bspwmrc Executable file
View file

@ -0,0 +1,68 @@
#!/usr/bin/env bash
# Sakura Line - bspwm.
# Colours, border width and gaps come from the theme (rice-theme); everything
# that runs for the session is started by rice-session. Machine-specific extras
# (autostarted apps, per-app desktops) go in ~/.config/rice/local.sh, which is
# not part of the repo.
export PATH="$HOME/.local/bin:$PATH"
export XDG_CURRENT_DESKTOP=bspwm
export QT_QPA_PLATFORMTHEME=qt6ct
export GTK_THEME=Adwaita:dark
[[ -f $HOME/.env ]] && . "$HOME/.env"
theme="${XDG_STATE_HOME:-$HOME/.local/state}/rice/theme"
# ---------------------------------------------------------------- monitors
primary=$(xrandr --query | awk '/ connected primary/ {print $1; exit}')
[[ -z $primary ]] && primary=$(bspc query -M --names | head -n1)
bspc monitor "$primary" -d 1 2 3 4 5 6
# Run the primary output at its highest mode and refresh rate.
mode=$(xrandr --query | awk -v m="$primary" '$1 == m {getline; print $1; exit}')
rate=$(xrandr --query | awk -v m="$primary" '$1 == m {getline; for (i = 2; i <= NF; i++) { r = $i; gsub(/[*+]/, "", r); if (r + 0 > best + 0) best = r }; print best; exit}')
[[ -n $mode && -n $rate ]] && xrandr --output "$primary" --mode "$mode" --rate "$rate"
# ------------------------------------------------------------------ layout
bspc config focus_follows_pointer true
bspc config borderless_monocle true
bspc config gapless_monocle false
bspc config split_ratio 0.52
bspc config automatic_scheme longest_side
bspc config initial_polarity second_child
bspc config pointer_modifier mod4
bspc config pointer_action1 move
bspc config pointer_action2 resize_side
bspc config pointer_action3 resize_corner
# colours, border width, gaps
[[ -f $theme/bspwm-colors.sh ]] && . "$theme/bspwm-colors.sh"
# ------------------------------------------------------------------- rules
# Restarting bspwm re-runs this file, so start from a clean rule list.
bspc rule -r '*'
for app in feh imv mpv vlc Yad Zenity Xarchiver ark Blueman-manager; do
bspc rule -a "$app" state=floating center=on
done
bspc rule -a Pavucontrol state=floating center=on rectangle=920x450+0+0
bspc rule -a qt5ct state=floating center=on rectangle=900x600+0+0
bspc rule -a qt6ct state=floating center=on rectangle=900x600+0+0
bspc rule -a Gnome-calculator state=floating center=on rectangle=360x500+0+0
bspc rule -a loupe state=floating center=on rectangle=1200x800+0+0
bspc rule -a '*:sun-awt-X11-XWindowPeer' manage=off
# rice surfaces
bspc rule -a rice-float state=floating center=on rectangle=900x560+0+0
bspc rule -a rice-about state=floating center=on rectangle=760x440+0+0
bspc rule -a rice-scratchpad state=floating sticky=on layer=above
bspc rule -a '*:rice-webcam' state=floating sticky=on layer=above border=off focus=off
bspc rule -a boomer state=fullscreen
# ------------------------------------------------------------------ session
xsetroot -cursor_name left_ptr
wmname LG3D
[[ -f $HOME/.config/rice/local.sh ]] && . "$HOME/.config/rice/local.sh"
rice-session start &

286
home/.config/btop/btop.conf Normal file
View file

@ -0,0 +1,286 @@
#? Config file for btop v.1.4.7
#* Name of a btop++/bpytop/bashtop formatted ".theme" file, "Default" and "TTY" for builtin themes.
#* Themes should be placed in "../share/btop/themes" relative to binary or "$HOME/.config/btop/themes"
color_theme = "rice"
#* If the theme set background should be shown, set to False if you want terminal background transparency.
theme_background = false
#* Sets if 24-bit truecolor should be used, will convert 24-bit colors to 256 color (6x6x6 color cube) if false.
truecolor = true
#* Set to true to force tty mode regardless if a real tty has been detected or not.
#* Will force 16-color mode and TTY theme, set all graph symbols to "tty" and swap out other non tty friendly symbols.
force_tty = false
#* Option to disable presets. Either the default preset, custom presets, or all presets.
#* "Off" All presets are enabled.
#* "Default" preset is disabled.#* "Custom" presets are disabled.#* "All" presets are disabled.
disable_presets = "Off"
#* Define presets for the layout of the boxes. Preset 0 is always all boxes shown with default settings. Max 9 presets.
#* Format: "box_name:P:G,box_name:P:G" P=(0 or 1) for alternate positions, G=graph symbol to use for box.
#* Use whitespace " " as separator between different presets.
#* Example: "cpu:0:default,mem:0:tty,proc:1:default cpu:0:braille,proc:0:tty"
presets = "cpu:1:default,proc:0:default cpu:0:default,mem:0:default,net:0:default cpu:0:block,net:0:tty"
#* Set to True to enable "h,j,k,l,g,G" keys for directional control in lists.
#* Conflicting keys for h:"help" and k:"kill" is accessible while holding shift.
vim_keys = false
#* Disable all mouse events.
disable_mouse = false
#* Rounded corners on boxes, is ignored if TTY mode is ON.
rounded_corners = true
#* Use terminal synchronized output sequences to reduce flickering on supported terminals.
terminal_sync = true
#* Default symbols to use for graph creation, "braille", "block" or "tty".
#* "braille" offers the highest resolution but might not be included in all fonts.
#* "block" has half the resolution of braille but uses more common characters.
#* "tty" uses only 3 different symbols but will work with most fonts and should work in a real TTY.
#* Note that "tty" only has half the horizontal resolution of the other two, so will show a shorter historical view.
graph_symbol = "braille"
# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty".
graph_symbol_cpu = "default"
# Graph symbol to use for graphs in gpu box, "default", "braille", "block" or "tty".
graph_symbol_gpu = "default"
# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty".
graph_symbol_mem = "default"
# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty".
graph_symbol_net = "default"
# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty".
graph_symbol_proc = "default"
#* Manually set which boxes to show. Available values are "cpu mem net proc" and "gpu0" through "gpu5", separate values with whitespace.
shown_boxes = "cpu mem net proc"
#* Update time in milliseconds, recommended 2000 ms or above for better sample times for graphs.
update_ms = 1000
#* Processes sorting, "pid" "program" "arguments" "threads" "user" "memory" "cpu lazy" "cpu direct",
#* "cpu lazy" sorts top process over time (easier to follow), "cpu direct" updates top process directly.
proc_sorting = "cpu lazy"
#* Reverse sorting order, True or False.
proc_reversed = false
#* Show processes as a tree.
proc_tree = false
#* Use the cpu graph colors in the process list.
proc_colors = true
#* Use a darkening gradient in the process list.
proc_gradient = true
#* If process cpu usage should be of the core it's running on or usage of the total available cpu power.
proc_per_core = false
#* Show process memory as bytes instead of percent.
proc_mem_bytes = true
#* Show cpu graph for each process.
proc_cpu_graphs = true
#* Use /proc/[pid]/smaps for memory information in the process info box (very slow but more accurate)
proc_info_smaps = false
#* Show proc box on left side of screen instead of right.
proc_left = false
#* (Linux) Filter processes tied to the Linux kernel(similar behavior to htop).
proc_filter_kernel = false
#* Should the process list follow the selected process when detailed view is open.
proc_follow_detailed = true
#* In tree-view, always accumulate child process resources in the parent process.
proc_aggregate = false
#* Should cpu and memory usage display be preserved for dead processes when paused.
keep_dead_proc_usage = false
#* Sets the CPU stat shown in upper half of the CPU graph, "total" is always available.
#* Select from a list of detected attributes from the options menu.
cpu_graph_upper = "total"
#* Sets the CPU stat shown in lower half of the CPU graph, "total" is always available.
#* Select from a list of detected attributes from the options menu.
cpu_graph_lower = "Auto"
#* If gpu info should be shown in the cpu box. Available values = "Auto", "On" and "Off".
show_gpu_info = "Auto"
#* Toggles if the lower CPU graph should be inverted.
cpu_invert_lower = true
#* Set to True to completely disable the lower CPU graph.
cpu_single_graph = false
#* Show cpu box at bottom of screen instead of top.
cpu_bottom = false
#* Shows the system uptime in the CPU box.
show_uptime = true
#* Shows the CPU package current power consumption in watts. Requires running `make setcap` or `make setuid` or running with sudo.
show_cpu_watts = true
#* Show cpu temperature.
check_temp = true
#* Which sensor to use for cpu temperature, use options menu to select from list of available sensors.
cpu_sensor = "Auto"
#* Show temperatures for cpu cores also if check_temp is True and sensors has been found.
show_coretemp = true
#* Set a custom mapping between core and coretemp, can be needed on certain cpus to get correct temperature for correct core.
#* Use lm-sensors or similar to see which cores are reporting temperatures on your machine.
#* Format "x:y" x=core with wrong temp, y=core with correct temp, use space as separator between multiple entries.
#* Example: "4:0 5:1 6:3"
cpu_core_map = ""
#* Which temperature scale to use, available values: "celsius", "fahrenheit", "kelvin" and "rankine".
temp_scale = "celsius"
#* Use base 10 for bits/bytes sizes, KB = 1000 instead of KiB = 1024.
base_10_sizes = false
#* Show CPU frequency.
show_cpu_freq = true
#* How to calculate CPU frequency, available values: "first", "range", "lowest", "highest" and "average".
freq_mode = "first"
#* Draw a clock at top of screen, formatting according to strftime, empty string to disable.
#* Special formatting: /host = hostname | /user = username | /uptime = system uptime
clock_format = "%H:%M"
#* Update main ui in background when menus are showing, set this to false if the menus is flickering too much for comfort.
background_update = true
#* Custom cpu model name, empty string to disable.
custom_cpu_name = ""
#* Optional filter for shown disks, should be full path of a mountpoint, separate multiple values with whitespace " ".
#* Only disks matching the filter will be shown. Prepend exclude= to only show disks not matching the filter. Examples: disk_filter="/boot /home/user", disks_filter="exclude=/boot /home/user"
disks_filter = ""
#* Show graphs instead of meters for memory values.
mem_graphs = true
#* Show mem box below net box instead of above.
mem_below_net = false
#* Count ZFS ARC in cached and available memory.
zfs_arc_cached = true
#* If swap memory should be shown in memory box.
show_swap = true
#* Show swap as a disk, ignores show_swap value above, inserts itself after first disk.
swap_disk = true
#* If mem box should be split to also show disks info.
show_disks = true
#* Filter out non physical disks. Set this to False to include network disks, RAM disks and similar.
only_physical = true
#* Read disks list from /etc/fstab. This also disables only_physical.
use_fstab = true
#* Setting this to True will hide all datasets, and only show ZFS pools. (IO stats will be calculated per-pool)
zfs_hide_datasets = false
#* Set to true to show available disk space for privileged users.
disk_free_priv = false
#* Toggles if io activity % (disk busy time) should be shown in regular disk usage view.
show_io_stat = true
#* Toggles io mode for disks, showing big graphs for disk read/write speeds.
io_mode = false
#* Set to True to show combined read/write io graphs in io mode.
io_graph_combined = false
#* Set the top speed for the io graphs in MiB/s (100 by default), use format "mountpoint:speed" separate disks with whitespace " ".
#* Example: "/mnt/media:100 /:20 /boot:1".
io_graph_speeds = ""
#* Swap the positions of the upload and download speed graphs. When true, upload will be on top.
swap_upload_download = false
#* Set fixed values for network graphs in Mebibits. Is only used if net_auto is also set to False.
net_download = 100
net_upload = 100
#* Use network graphs auto rescaling mode, ignores any values set above and rescales down to 10 Kibibytes at the lowest.
net_auto = true
#* Sync the auto scaling for download and upload to whichever currently has the highest scale.
net_sync = true
#* Starts with the Network Interface specified here.
net_iface = ""
#* "True" shows bitrates in base 10 (Kbps, Mbps). "False" shows bitrates in binary sizes (Kibps, Mibps, etc.). "Auto" uses base_10_sizes.
base_10_bitrate = "Auto"
#* Show battery stats in top right if battery is present.
show_battery = true
#* Which battery to use if multiple are present. "Auto" for auto detection.
selected_battery = "Auto"
#* Show power stats of battery next to charge indicator.
show_battery_watts = true
#* Set loglevel for "~/.local/state/btop.log" levels are: "ERROR" "WARNING" "INFO" "DEBUG".
#* The level set includes all lower levels, i.e. "DEBUG" will show all logging info.
log_level = "WARNING"
#* Automatically save current settings to config file on exit.
save_config_on_exit = false
#* Measure PCIe throughput on NVIDIA cards, may impact performance on certain cards.
nvml_measure_pcie_speeds = true
#* Measure PCIe throughput on AMD cards, may impact performance on certain cards.
rsmi_measure_pcie_speeds = true
#* Horizontally mirror the GPU graph.
gpu_mirror_graph = true
#* Set which GPU vendors to show. Available values are "nvidia amd intel apple"
shown_gpus = "nvidia amd intel apple"
#* Custom gpu0 model name, empty string to disable.
custom_gpu_name0 = ""
#* Custom gpu1 model name, empty string to disable.
custom_gpu_name1 = ""
#* Custom gpu2 model name, empty string to disable.
custom_gpu_name2 = ""
#* Custom gpu3 model name, empty string to disable.
custom_gpu_name3 = ""
#* Custom gpu4 model name, empty string to disable.
custom_gpu_name4 = ""
#* Custom gpu5 model name, empty string to disable.
custom_gpu_name5 = ""

102
home/.config/dunst/dunstrc Normal file
View file

@ -0,0 +1,102 @@
# Sakura Line - dunst behaviour. Colours and fonts come from the theme drop-in
# (dunstrc.d/50-theme.conf -> ~/.local/state/rice/theme/dunst.conf).
[global]
monitor = 0
follow = mouse
origin = top-right
# Same outer edges as tiled windows and popups. bspwm draws borders inside the
# gap, so that is one gap (10px) in from the right and bar (37px) + gap down.
offset = (10, 47)
width = (300, 400)
height = (0, 300)
notification_limit = 5
gap_size = 6
padding = 12
horizontal_padding = 14
text_icon_padding = 12
frame_width = 1
corner_radius = 12
progress_bar = true
progress_bar_height = 6
progress_bar_frame_width = 0
progress_bar_min_width = 150
progress_bar_max_width = 372
progress_bar_corner_radius = 3
indicate_hidden = yes
sort = urgency_descending
idle_threshold = 120
line_height = 2
markup = full
format = "<b>%s</b>\n%b"
alignment = left
vertical_alignment = center
show_age_threshold = 60
ellipsize = middle
word_wrap = yes
ignore_newline = no
stack_duplicates = true
hide_duplicate_count = false
show_indicators = no
enable_recursive_icon_lookup = true
icon_position = left
min_icon_size = 32
max_icon_size = 48
sticky_history = yes
history_length = 50
always_run_script = true
dmenu = /usr/bin/rofi -dmenu -theme ~/.config/rofi/menu.rasi -p "Actions"
browser = /usr/bin/xdg-open
title = Dunst
class = Dunst
default_pause_level = 0
mouse_left_click = do_action, close_current
mouse_middle_click = close_all
mouse_right_click = close_current
[urgency_low]
timeout = 4
[urgency_normal]
timeout = 6
[urgency_critical]
timeout = 0
# Do-not-disturb (rice-notify dnd) sets pause level 60. Your own confirmations
# and critical alerts sent from the command line still get through; chat apps
# that mark everything critical use their own app name, so they stay quiet.
[rice_feedback]
appname = "rice"
override_pause_level = 100
[critical_cli]
appname = "notify-send"
msg_urgency = critical
override_pause_level = 100
# Volume, brightness and recording progress: replaced in place, never kept.
[osd]
appname = "osd"
format = "<b>%s</b>"
icon_position = off
timeout = 2
history_ignore = yes
override_pause_level = 100
# Every other notification pokes the notification centre so its count and list stay live.
[notify_centre]
appname = "*"
script = ~/.local/bin/rice-notify-hook
[fullscreen_delay]
fullscreen = delay
[fullscreen_critical]
msg_urgency = critical
fullscreen = show
[flameshot_noise]
body = "grim's screenshot component is implemented based on wlroots*"
skip_display = true
history_ignore = yes

217
home/.config/eww/eww.scss Normal file
View file

@ -0,0 +1,217 @@
// Sakura Line - eww design system.
// The same tokens as polybar, rofi and dunst: the theme's alpha, 1px border,
// 12px radius, rows with an 8px radius and a 17% accent tint on hover.
// colors.scss is a link to ~/.local/state/rice/theme/eww-colors.scss.
@import "colors";
$tint: rgba($accent, 0.17);
* {
all: unset;
font-family: $font, "Symbols Nerd Font";
}
window, .closer { background-color: transparent; }
tooltip {
background-color: rgba($bg, 0.95);
border: $border-width solid $border;
border-radius: $radius-small;
padding: 6px 10px;
color: $fg;
font-size: 11px;
}
// ------------------------------------------------------------------ panel
.panel {
background-color: rgba($bg, $alpha);
border: $border-width solid $border;
border-radius: $radius;
padding: 4px 8px 10px 8px;
color: $fg;
font-size: 13px;
}
.head {
padding: 10px 10px 12px 10px;
margin-bottom: 6px;
border-bottom: $border-width solid $border;
}
.title { font-weight: bold; font-size: 14px; }
.head-action {
color: $dim;
font-size: 15px;
padding: 2px 7px;
margin-left: 2px;
border-radius: $radius-small;
}
.head-action:hover { color: $fg; background-color: $tint; }
.head-action.active { color: $accent; }
.section {
color: $dim;
font-size: 11px;
font-weight: 600;
padding: 10px 10px 4px 10px;
}
.sep {
min-height: 1px;
margin: 6px 4px;
background-color: $border;
}
.empty { color: $dim; padding: 8px 10px; }
// -------------------------------------------------------------------- rows
.row {
padding: 8px 10px;
border-radius: $radius-small;
}
.row:hover { background-color: $tint; }
.row.active .row-title, .row.active .glyph { color: $accent; }
.row.dimmed .row-title, .row.dimmed .glyph { color: $dim; }
.glyph { font-size: 15px; min-width: 24px; margin-right: 10px; }
.row-title { color: $fg; }
.row-sub { color: $dim; font-size: 11px; margin-top: 1px; }
.row-value { color: $dim; margin-left: 10px; }
.chev { color: $dim; margin-left: 8px; font-size: 15px; }
.accent { color: $accent; }
.urgent { color: $urgent; }
.good { color: $green; }
.warn { color: $yellow; }
.dim { color: $dim; }
// ---------------------------------------------------------------- controls
.slider-row:hover { background-color: transparent; }
.slider-row .glyph { padding: 2px 0; border-radius: $radius-small; }
.slider-row .glyph:hover { color: $accent; }
.slider-value { min-width: 48px; }
scale trough {
min-height: 6px;
min-width: 140px;
margin: 0 4px;
border-radius: 3px;
background-color: $selection;
}
scale highlight {
border-radius: 3px;
background-color: $accent;
}
scale slider {
min-width: 12px;
min-height: 12px;
margin: -3px;
border-radius: 6px;
background-color: $fg;
}
.switch {
min-width: 30px;
min-height: 16px;
padding: 2px;
border-radius: 10px;
background-color: $selection;
}
.switch.on { background-color: rgba($accent, 0.55); }
.knob {
min-width: 12px;
min-height: 12px;
border-radius: 6px;
background-color: $dim;
}
.switch.on .knob { background-color: $fg; }
.pill {
padding: 4px 10px;
margin: 0 3px;
border-radius: $radius-small;
color: $dim;
}
.pills { padding: 2px 6px 6px 6px; }
.pill:hover { color: $fg; background-color: $tint; }
.pill.active { color: $accent; background-color: $tint; }
.field {
padding: 8px 10px;
border-radius: $radius-small;
background-color: rgba($surface, 0.8);
color: $fg;
caret-color: $accent;
}
progressbar trough {
min-height: 4px;
border-radius: 2px;
background-color: $selection;
}
progressbar progress {
min-height: 4px;
border-radius: 2px;
background-color: $accent;
}
// ---------------------------------------------------------------- calendar
.cal-title { font-weight: bold; }
.cal-grid { margin: 4px 4px 2px 4px; }
.cal-dow, .cal-wk { color: $dim; font-size: 10px; font-weight: bold; min-height: 22px; }
.cal-wk { min-width: 26px; }
.cal-day {
min-width: 34px;
min-height: 30px;
border-radius: $radius-small;
color: $fg;
}
.cal-day:hover { background-color: $tint; }
.cal-day.other { color: $muted; }
.cal-day.weekend { color: $fg-alt; }
.cal-day.selected { background-color: $tint; color: $accent; }
.cal-day.today { background-color: $accent; color: $bg; font-weight: bold; }
.cal-dot { color: $magenta; font-size: 8px; min-height: 8px; }
.cal-day.today .cal-dot { color: $bg; }
.event-time { color: $accent; min-width: 52px; font-size: 12px; }
.event-bar { min-width: 3px; border-radius: 2px; margin-right: 10px; background-color: $magenta; }
.event-bar.reminder { background-color: $yellow; }
// ------------------------------------------------------------------- media
.art {
min-width: 72px;
min-height: 72px;
border-radius: $radius-small;
background-size: cover;
background-position: center;
background-color: $surface;
}
.media-title { font-weight: bold; }
.media-artist { color: $dim; font-size: 12px; }
.media-btn {
font-size: 18px;
padding: 4px 12px;
border-radius: $radius-small;
}
.media-btn:hover { background-color: $tint; color: $accent; }
.media-time { color: $dim; font-size: 10px; }
// ----------------------------------------------------------- notifications
// The event box takes the hover tint; GTK ignores padding on event boxes,
// so the box inside it carries the padding.
.notif-hit { border-radius: $radius-small; }
.notif-hit:hover { background-color: $tint; }
.notif { padding: 8px 10px; }
.notif.critical .notif-summary { color: $urgent; }
.notif-app { color: $dim; font-size: 10px; font-weight: bold; }
.notif-summary { font-weight: bold; }
.notif-body { color: $fg-alt; font-size: 12px; }
.notif-ago { color: $dim; font-size: 10px; }
.notif-close {
color: $dim;
padding: 0 6px;
border-radius: $radius-small;
font-size: 13px;
}
.notif-close:hover { color: $urgent; background-color: rgba($urgent, 0.15); }

91
home/.config/eww/eww.yuck Normal file
View file

@ -0,0 +1,91 @@
;; Sakura Line - eww popups.
;; rice-panel opens one window at a time; every panel is built from the
;; widgets below so they all look and behave like the rofi menus.
;; ------------------------------------------------------------------ state
(defvar open_panel "") ; set by rice-panel; list polls only run while their panel is open
(defvar cal_offset 0) ; months away from the current one
(defvar cal_day "") ; selected day as YYYY-MM-DD, "" = today
(defvar cal_input "") ; text typed into the calendar's add box
(deflisten notifs :initial "{}" "rice-notify watch")
(deflisten audio :initial "{}" "rice-audio watch")
(deflisten media :initial "{}" "rice-media watch")
(defpoll net :interval "4s" :initial "{}" :run-while {open_panel == "network" || open_panel == "hub"} "rice-net list")
(defpoll bt :interval "4s" :initial "{}" :run-while {open_panel == "bluetooth" || open_panel == "hub"} "rice-bt list")
(defpoll display :interval "10s" :initial "{}" :run-while {open_panel == "display" || open_panel == "hub"} "rice-display info")
(defpoll power :interval "3s" :initial "{}" :run-while {open_panel == "power" || open_panel == "hub"} "rice-power info")
;; rice-cal reads cal_offset / cal_day itself and pushes fresh data on every click.
(defpoll month :interval "60s" :initial "{}" :run-while {open_panel == "calendar"} "rice-cal month")
(defpoll agenda :interval "30s" :initial "{}" :run-while {open_panel == "calendar"} "rice-cal agenda")
(defpoll idle :interval "3s" :initial "{}" :run-while {open_panel == "hub"} "rice-idle status")
;; ---------------------------------------------------------------- windows
;; A transparent layer under the open panel: clicking anywhere else closes it.
(defwindow closer
:monitor 0
:geometry (geometry :x "0" :y "0" :width "100%" :height "100%")
:stacking "fg"
:wm-ignore true
(eventbox :class "closer" :onclick "rice-panel close" :onrightclick "rice-panel close"))
;; -------------------------------------------------------- shared widgets
;; Header: title on the left, optional action buttons on the right.
(defwidget head [title]
(box :class "head" :space-evenly false
(label :class "title" :text title :hexpand true :xalign 0)
(children)))
(defwidget head-action [glyph onclick ?tip ?active]
(button :class "head-action ${active == true ? 'active' : ''}" :onclick onclick :tooltip {tip ?: ""} glyph))
(defwidget section [text]
(label :class "section" :xalign 0 :text text))
(defwidget sep []
(box :class "sep"))
;; One list row: glyph, title and optional subtitle, value on the right.
(defwidget row [glyph title ?sub ?value ?onclick ?active ?chev ?dim]
(button :class "row ${active == true ? 'active' : ''} ${dim == true ? 'dimmed' : ''}" :onclick {onclick ?: ""}
(box :space-evenly false
(label :class "glyph" :text glyph)
(box :orientation "v" :hexpand true :space-evenly false :valign "center"
(label :class "row-title" :xalign 0 :limit-width 30 :text title)
(label :class "row-sub" :xalign 0 :limit-width 38 :visible {(sub ?: "") != ""} :text {sub ?: ""}))
(label :class "row-value" :visible {(value ?: "") != ""} :text {value ?: ""})
(label :class "chev" :visible {chev == true} :text ""))))
;; A row with a pill switch.
(defwidget switch-row [glyph title on onclick ?sub]
(button :class "row" :onclick onclick
(box :space-evenly false
(label :class "glyph ${on == true ? 'accent' : ''}" :text glyph)
(box :orientation "v" :hexpand true :space-evenly false :valign "center"
(label :class "row-title" :xalign 0 :text title)
(label :class "row-sub" :xalign 0 :visible {(sub ?: "") != ""} :text {sub ?: ""}))
(box :class "switch ${on == true ? 'on' : ''}" :valign "center" :space-evenly false
(box :class "knob" :hexpand true :halign {on == true ? "end" : "start"})))))
;; A slider row: clicking the glyph runs onglyph (mute), dragging runs onchange.
(defwidget slider-row [glyph value onchange ?onglyph ?muted]
(box :class "row slider-row" :space-evenly false
(button :class "glyph ${muted == true ? 'urgent' : ''}" :onclick {onglyph ?: ""} glyph)
(scale :class "slider" :hexpand true :min 0 :max 101 :value {value ?: 0} :onchange onchange)
(label :class "row-value slider-value" :xalign 1 :text {muted == true ? "muted" : "${value ?: 0}%"})))
;; An empty-state line inside a list.
(defwidget empty [text]
(label :class "empty" :xalign 0 :text text))
;; ----------------------------------------------------------------- panels
(include "panels/hub.yuck")
(include "panels/audio.yuck")
(include "panels/network.yuck")
(include "panels/bluetooth.yuck")
(include "panels/display.yuck")
(include "panels/power.yuck")
(include "panels/media.yuck")
(include "panels/calendar.yuck")
(include "panels/notifications.yuck")

View file

@ -0,0 +1,3 @@
# sxhkd config that rice-panel runs only while a panel is open.
Escape
rice-panel close

View file

@ -0,0 +1,45 @@
;; Audio: output and mic levels, device pickers, per-app mixer.
(defwindow audio
:monitor 0
:geometry (geometry :x "-10px" :y "47px" :width "380px" :anchor "top right")
:stacking "fg"
:wm-ignore true
:resizable false
(box :class "panel" :orientation "v" :space-evenly false
(head :title "Audio"
(head-action :glyph "󰒓" :tip "Advanced mixer" :onclick "rice-panel close; setsid -f pavucontrol"))
(slider-row :glyph {audio?.sink?.muted == true ? "󰝟" : "󰕾"}
:value {audio?.sink?.vol ?: 0}
:muted {audio?.sink?.muted ?: false}
:onglyph "rice-volume mute"
:onchange "rice-volume set {}")
(slider-row :glyph {audio?.source?.muted == true ? "󰍭" : "󰍬"}
:value {audio?.source?.vol ?: 0}
:muted {audio?.source?.muted ?: false}
:onglyph "rice-audio mic-mute"
:onchange "rice-audio mic {}")
;; eww appends a loop's items after their siblings, so every loop gets its own box.
(section :text "Output")
(box :orientation "v" :space-evenly false
(for s in {audio?.sinks ?: []}
(row :glyph "󰓃" :title {s.desc} :active {s.default} :onclick "rice-audio sink ${s.index}")))
(section :text "Input")
(box :orientation "v" :space-evenly false
(for s in {audio?.sources ?: []}
(row :glyph "󰍬" :title {s.desc} :active {s.default} :onclick "rice-audio source ${s.index}")))
(box :orientation "v" :space-evenly false :visible {arraylength(audio?.apps ?: []) > 0}
(section :text "Apps")
(box :orientation "v" :space-evenly false
(for a in {audio?.apps ?: []}
(box :class "row slider-row" :space-evenly false
(button :class "glyph ${a.muted ? 'urgent' : ''}" :tooltip {a.name}
:onclick "rice-audio app-mute ${a.id}"
{a.muted ? "󰝟" : "󰎆"})
(label :class "row-title" :xalign 0 :width 90 :limit-width 11 :text {a.name})
(scale :class "slider" :hexpand true :min 0 :max 101 :value {a.vol}
:onchange "rice-audio app-volume ${a.id} {}")
(label :class "row-value slider-value" :xalign 1 :text "${a.vol}%")))))))

View file

@ -0,0 +1,32 @@
;; Bluetooth: power, paired devices with battery, nearby devices to pair.
(defwindow bluetooth
:monitor 0
:geometry (geometry :x "-10px" :y "47px" :width "380px" :anchor "top right")
:stacking "fg"
:wm-ignore true
:resizable false
(box :class "panel" :orientation "v" :space-evenly false
(head :title "Bluetooth"
(head-action :glyph "󰑓" :tip "Look for new devices" :active {bt?.discovering == true}
:onclick "rice-bt scan"))
(switch-row :glyph "󰂯" :title "Bluetooth" :on {bt?.powered == true}
:onclick "rice-bt power toggle"
:sub {bt?.powered != true ? "off" : (bt?.connected > 0 ? "${bt.connected} connected" : "on")})
(box :orientation "v" :space-evenly false :visible {bt?.powered == true}
(section :text "My devices")
(box :orientation "v" :space-evenly false
(for d in {bt?.paired ?: []}
(row :glyph {d.glyph} :title {d.name} :sub {d.sub} :value {d.battery}
:active {d.connected} :onclick "rice-bt toggle ${d.mac}")))
(box :visible {arraylength(bt?.paired ?: []) == 0}
(empty :text "No paired devices"))
(section :text {bt?.discovering == true ? "Nearby · scanning" : "Nearby"})
(box :orientation "v" :space-evenly false
(for d in {bt?.nearby ?: []}
(row :glyph {d.glyph} :title {d.name} :sub {d.sub} :dim true
:onclick "rice-bt toggle ${d.mac}")))
(box :visible {arraylength(bt?.nearby ?: []) == 0}
(empty :text {bt?.discovering == true ? "Looking…" : "Press 󰑓 to look for devices"})))))

View file

@ -0,0 +1,47 @@
;; Calendar: month grid with ISO weeks, the selected day's events and reminders.
(defwindow calendar
:monitor 0
:geometry (geometry :x "0" :y "47px" :width "360px" :anchor "top center")
:stacking "fg"
:wm-ignore true
:resizable false
(box :class "panel" :orientation "v" :space-evenly false
(head :title {month?.title ?: "Calendar"}
(head-action :glyph "󰅁" :tip "Previous month" :onclick "rice-cal shift -1")
(head-action :glyph "󰃭" :tip "Today" :onclick "rice-cal today")
(head-action :glyph "󰅂" :tip "Next month" :onclick "rice-cal shift 1"))
(box :class "cal-grid" :orientation "v" :space-evenly false
(box :space-evenly false
(label :class "cal-wk" :text "Wk")
(for d in {month?.dow ?: []}
(label :class "cal-dow" :hexpand true :text d)))
(for w in {month?.weeks ?: []}
(box :space-evenly false
(label :class "cal-wk" :text {w.wk})
(for d in {w.days}
(button :hexpand true :onclick "rice-cal select ${d.date}"
:class "cal-day ${d.other ? 'other' : ''} ${d.weekend ? 'weekend' : ''} ${d.selected ? 'selected' : ''} ${d.today ? 'today' : ''}"
(box :orientation "v" :space-evenly false :valign "center"
(label :text {d.d})
(label :class "cal-dot" :text {d.events > 0 ? "●" : " "})))))))
(sep)
(box :space-evenly false
(label :class "section" :hexpand true :xalign 0 :text {agenda?.heading ?: "Today"})
(head-action :glyph "󰃅" :tip "Add event" :onclick "rice-cal add")
(head-action :glyph "󰀠" :tip "Add reminder" :onclick "rice-panel close; rice-reminder new"))
(box :orientation "v" :space-evenly false
(for ev in {agenda?.items ?: []}
(box :class "row" :space-evenly false
(box :class "event-bar ${ev.kind == 'reminder' ? 'reminder' : ''}")
(label :class "event-time" :xalign 0 :text {ev.time})
(box :orientation "v" :hexpand true :space-evenly false :valign "center"
(label :class "row-title" :xalign 0 :limit-width 28 :text {ev.title})
(label :class "row-sub" :xalign 0 :limit-width 36 :visible {ev.sub != ""} :text {ev.sub}))
(button :class "notif-close" :valign "center" :visible {ev.kind == "reminder"}
:tooltip "Cancel reminder" :onclick "rice-reminder cancel ${ev.id}" "󰅖"))))
(box :orientation "v" :space-evenly false :visible {arraylength(agenda?.items ?: []) == 0}
(empty :text {(agenda?.status ?: "") != "" ? agenda.status : "Nothing planned"}))))

View file

@ -0,0 +1,30 @@
;; Display: brightness over DDC, resolution, refresh rate, bar, style shortcuts.
(defwindow display
:monitor 0
:geometry (geometry :x "-10px" :y "47px" :width "380px" :anchor "top right")
:stacking "fg"
:wm-ignore true
:resizable false
(box :class "panel" :orientation "v" :space-evenly false
(head :title "Display"
(head-action :glyph "󰸉" :tip "Wallpaper" :onclick "rice-panel close; rice-menu wallpaper")
(head-action :glyph "󰏘" :tip "Theme" :onclick "rice-panel close; rice-menu theme"))
(box :orientation "v" :space-evenly false :visible {display?.ddc == true}
(slider-row :glyph "󰃠" :value {display?.brightness ?: 0}
:onchange "rice-display brightness {}"))
(box :orientation "v" :space-evenly false :visible {display?.ddc != true}
(row :glyph "󰃠" :title "Brightness" :sub {display?.ddcHint ?: ""} :dim true))
(section :text {display?.output ?: "Output"})
(box :orientation "v" :space-evenly false
(for m in {display?.modes ?: []}
(row :glyph "󰍹" :title {m.mode} :active {m.current} :onclick "rice-display mode ${m.id}")))
(section :text "Refresh rate")
(box :class "pills" :space-evenly true
(for r in {display?.rates ?: []}
(button :class "pill ${r.current ? 'active' : ''}" :onclick "rice-display rate ${r.id}" {r.label})))
(sep)
(switch-row :glyph "󰍜" :title "Bar" :on {display?.bar == true} :onclick "rice-bar toggle")))

View file

@ -0,0 +1,50 @@
;; Control centre (super + x): levels, a line per subsystem, quick switches, now playing.
(defwindow hub
:monitor 0
:geometry (geometry :x "-10px" :y "47px" :width "400px" :anchor "top right")
:stacking "fg"
:wm-ignore true
:resizable false
(box :class "panel" :orientation "v" :space-evenly false
(head :title "Control Centre"
(head-action :glyph "󰍜" :tip "Rice menu" :onclick "rice-panel close; rice-menu main")
(head-action :glyph "󰐥" :tip "Power menu" :onclick "rice-panel close; rice-menu power"))
(slider-row :glyph {audio?.sink?.muted == true ? "󰝟" : "󰕾"}
:value {audio?.sink?.vol ?: 0} :muted {audio?.sink?.muted ?: false}
:onglyph "rice-volume mute" :onchange "rice-volume set {}")
(slider-row :glyph {audio?.source?.muted == true ? "󰍭" : "󰍬"}
:value {audio?.source?.vol ?: 0} :muted {audio?.source?.muted ?: false}
:onglyph "rice-audio mic-mute" :onchange "rice-audio mic {}")
(box :orientation "v" :space-evenly false :visible {display?.ddc == true}
(slider-row :glyph "󰃠" :value {display?.brightness ?: 0} :onchange "rice-display brightness {}"))
(sep)
(row :glyph {net?.active?.type == "ethernet" ? "󰈀" : (net?.active?.type == "wifi" ? "󰖩" : "󰖪")}
:title "Network" :value {net?.active?.name ?: "offline"} :chev true :onclick "rice-panel network")
(row :glyph "󰂯" :title "Bluetooth" :chev true :onclick "rice-panel bluetooth"
:value {bt?.powered != true ? "off" : (bt?.connected > 0 ? "${bt.connected} connected" : "on")})
(row :glyph "󰓃" :title "Sound" :value {audio?.sink?.name ?: ""} :chev true :onclick "rice-panel audio")
(row :glyph "󰍹" :title "Display" :chev true :onclick "rice-panel display"
:value "${display?.mode ?: ''} · ${round(display?.rate ?: 0, 0)} Hz")
(row :glyph "󰍛" :title "System" :chev true :onclick "rice-panel power"
:value "CPU ${power?.cpu?.load ?: 0}% · Memory ${power?.memory?.percent ?: 0}%")
(row :glyph "󰃭" :title "Calendar" :value {formattime(EWW_TIME, "%a %-d %b")} :chev true :onclick "rice-panel calendar")
(sep)
(switch-row :glyph "󰂛" :title "Do not disturb" :on {notifs?.dnd == true} :onclick "rice-notify dnd toggle"
:sub {(notifs?.count ?: 0) > 0 ? "${notifs.count} in the notification centre" : ""})
(switch-row :glyph "󰅶" :title "Stay awake" :on {idle?.awake == true} :onclick "rice-idle toggle"
:sub {idle?.awake == true ? "screensaver and lock paused" : ""})
(box :orientation "v" :space-evenly false :visible {media?.status == "Playing" || media?.status == "Paused"}
(sep)
(box :class "row" :space-evenly false
(label :class "glyph accent" :text "󰎆")
(eventbox :hexpand true :onclick "rice-panel media"
(box :orientation "v" :space-evenly false :valign "center"
(label :class "row-title" :xalign 0 :limit-width 22 :text {media?.title ?: ""})
(label :class "row-sub" :xalign 0 :limit-width 26 :text {media?.artist ?: ""})))
(button :class "media-btn" :onclick "playerctl previous" "󰒮")
(button :class "media-btn accent" :onclick "playerctl play-pause" {media?.status == "Playing" ? "󰏤" : "󰐊"})
(button :class "media-btn" :onclick "playerctl next" "󰒭")))))

View file

@ -0,0 +1,39 @@
;; Media: what is playing, a seek bar, and transport controls.
(defpoll media_pos :interval "1s" :initial "{}" :run-while {open_panel == "media"} "rice-media position")
(defwindow media
:monitor 0
:geometry (geometry :x "0" :y "47px" :width "380px" :anchor "top center")
:stacking "fg"
:wm-ignore true
:resizable false
(box :class "panel" :orientation "v" :space-evenly false
(head :title "Media"
(label :class "dim" :text {media?.player ?: ""}))
(box :orientation "v" :space-evenly false
:visible {media?.status != "Playing" && media?.status != "Paused"}
(empty :text "Nothing is playing"))
(box :orientation "v" :space-evenly false
:visible {media?.status == "Playing" || media?.status == "Paused"}
(box :class "row" :space-evenly false
(box :class "art" :visible {(media?.art ?: "") != ""}
:style "background-image: url('${media?.art ?: ''}');")
(box :orientation "v" :space-evenly false :valign "center" :hexpand true
(label :class "media-title" :xalign 0 :limit-width 26 :text {media?.title ?: ""})
(label :class "media-artist" :xalign 0 :limit-width 32 :text {media?.artist ?: ""})
(label :class "media-artist" :xalign 0 :limit-width 32 :visible {(media?.album ?: "") != ""}
:text {media?.album ?: ""})))
(box :class "row slider-row" :space-evenly false :visible {(media?.length ?: 0) > 0}
(label :class "media-time" :width 36 :xalign 0 :text {media_pos?.text ?: "0:00"})
(scale :class "slider" :hexpand true :min 0 :max {media?.length ?: 1}
:value {media_pos?.pos ?: 0} :onchange "rice-media seek {}")
(label :class "media-time" :width 36 :xalign 1 :text {media?.lengthText ?: ""}))
(box :class "row" :space-evenly true
(button :class "media-btn" :tooltip "Previous" :onclick "playerctl previous" "󰒮")
(button :class "media-btn accent" :tooltip "Play / pause" :onclick "playerctl play-pause"
{media?.status == "Playing" ? "󰏤" : "󰐊"})
(button :class "media-btn" :tooltip "Next" :onclick "playerctl next" "󰒭")))))

View file

@ -0,0 +1,37 @@
;; Network: Wi-Fi radio, the active connection, nearby networks and VPNs.
(defwindow network
:monitor 0
:geometry (geometry :x "-10px" :y "47px" :width "380px" :anchor "top right")
:stacking "fg"
:wm-ignore true
:resizable false
(box :class "panel" :orientation "v" :space-evenly false
(head :title "Network"
(head-action :glyph "󰑓" :tip "Rescan" :onclick "rice-net rescan")
(head-action :glyph "󰐲" :tip "Share Wi-Fi as a QR code" :onclick "rice-panel close; rice-net share")
(head-action :glyph "󰒓" :tip "Connection editor" :onclick "rice-panel close; setsid -f nm-connection-editor"))
(box :orientation "v" :space-evenly false :visible {net?.active?.type == "ethernet"}
(row :glyph "󰈀" :title "Ethernet" :sub {net?.active?.name ?: ""} :active true))
;; Machines without a Wi-Fi adapter (like a wired desktop) skip this part.
(box :orientation "v" :space-evenly false :visible {net?.hasWifi == true}
(switch-row :glyph "󰖩" :title "Wi-Fi" :on {net?.radio == true}
:onclick "rice-net radio toggle"
:sub {net?.active?.type == "wifi" ? net.active.name : (net?.radio == true ? "not connected" : "off")}))
(box :orientation "v" :space-evenly false :visible {net?.hasWifi == true && net?.radio == true}
(section :text "Networks")
(box :orientation "v" :space-evenly false
(for w in {net?.wifi ?: []}
(row :glyph {w.glyph} :title {w.ssid} :sub {w.sub} :active {w.active}
:onclick {w.active ? "rice-net disconnect" : "rice-net connect ${w.id}"})))
(box :visible {arraylength(net?.wifi ?: []) == 0}
(empty :text "No networks found yet")))
(box :orientation "v" :space-evenly false :visible {arraylength(net?.vpn ?: []) > 0}
(section :text "VPN")
(box :orientation "v" :space-evenly false
(for v in {net?.vpn ?: []}
(row :glyph {v.active ? "󰖂" : "󰕥"} :title {v.name} :sub {v.active ? "connected" : ""}
:active {v.active} :onclick "rice-net vpn ${v.id}"))))))

View file

@ -0,0 +1,37 @@
;; Notification centre: recent notifications, dismiss one or all, do-not-disturb.
(defwindow notifications
:monitor 0
:geometry (geometry :x "-10px" :y "47px" :width "380px" :anchor "top right")
:stacking "fg"
:wm-ignore true
:resizable false
(box :class "panel" :orientation "v" :space-evenly false
(head :title "Notifications"
(head-action :glyph {notifs?.dnd == true ? "󰂛" : "󰂚"} :active {notifs?.dnd == true}
:tip "Do not disturb" :onclick "rice-notify dnd toggle")
(head-action :glyph "󰎟" :tip "Clear all" :onclick "rice-notify clear"))
(box :orientation "v" :space-evenly false :visible {notifs?.dnd == true && (notifs?.waiting ?: 0) > 0}
(row :glyph "󰂛" :title "Held back" :sub "They appear when do-not-disturb is off"
:value "${notifs?.waiting ?: 0}" :dim true))
(box :orientation "v" :space-evenly false :visible {(notifs?.count ?: 0) == 0}
(empty :text "You're all caught up"))
(box :orientation "v" :space-evenly false
(for n in {notifs?.items ?: []}
(eventbox :class "notif-hit"
:onclick "rice-panel close; rice-notify open ${n.id}"
(box :class "notif ${n.urgency == 'CRITICAL' ? 'critical' : ''}" :space-evenly false
(box :orientation "v" :hexpand true :space-evenly false
(box :space-evenly false
(label :class "notif-app" :xalign 0 :hexpand true :limit-width 30 :text {n.app})
(label :class "notif-ago" :text {n.ago}))
(label :class "notif-summary" :xalign 0 :limit-width 36 :text {n.summary})
(label :class "notif-body" :xalign 0 :wrap true :limit-width 140
:visible {n.body != ""} :text {n.body}))
(button :class "notif-close" :valign "start" :tooltip "Dismiss"
:onclick "rice-notify remove ${n.id}" "󰅖")))))
(box :orientation "v" :space-evenly false :visible {(notifs?.count ?: 0) > 8}
(row :glyph "󰇘" :title "${notifs?.count - 8} older" :sub "Clear all to start fresh" :dim true))))

View file

@ -0,0 +1,31 @@
;; Power: what the machine is doing, the power profile, and session actions.
(defwindow power
:monitor 0
:geometry (geometry :x "-10px" :y "47px" :width "380px" :anchor "top right")
:stacking "fg"
:wm-ignore true
:resizable false
(box :class "panel" :orientation "v" :space-evenly false
(head :title "Power"
(head-action :glyph "󰍛" :tip "Activity monitor" :onclick "rice-panel close; setsid -f st -c rice-float -e btop"))
(row :glyph "󰍛" :title "CPU"
:value "${power?.cpu?.load ?: 0}%${power?.cpu?.temp != 'null' && power?.cpu?.temp != '' ? ' ' + power.cpu.temp + '°' : ''}")
(row :glyph "󰢮" :title "GPU" :sub {power?.gpu?.vram ?: ""}
:value "${power?.gpu?.load ?: 0}%${power?.gpu?.temp != 'null' && power?.gpu?.temp != '' ? ' ' + power.gpu.temp + '°' : ''}")
(row :glyph "󰘚" :title "Memory" :value "${power?.memory?.used ?: '-'} / ${power?.memory?.total ?: '-'}")
(row :glyph "󰔟" :title "Uptime" :value {power?.uptime ?: ""})
(box :orientation "v" :space-evenly false :visible {arraylength(power?.profiles ?: []) > 0}
(section :text "Power profile")
(box :class "row" :space-evenly true
(for p in {power?.profiles ?: []}
(button :class "pill ${p.active ? 'active' : ''}" :onclick "rice-power profile ${p.name}" {p.label}))))
(sep)
(box :space-evenly true :class "row"
(button :class "media-btn" :tooltip "Lock" :onclick "rice-power lock" "󰌾")
(button :class "media-btn" :tooltip "Suspend" :onclick "rice-power suspend" "󰒲")
(button :class "media-btn" :tooltip "Log out" :onclick "rice-power logout" "󰍃")
(button :class "media-btn" :tooltip "Reboot" :onclick "rice-power reboot" "󰜉")
(button :class "media-btn urgent" :tooltip "Shut down" :onclick "rice-power shutdown" "󰐥"))))

View file

@ -0,0 +1,42 @@
{
"$schema": "https://github.com/fastfetch-cli/fastfetch/raw/dev/doc/json_schema.json",
"logo": null,
"display": {
"separator": " "
},
"modules": [
"break",
{
"type": "os",
"key": "OS ",
"keyColor": "31", // = color1
},
{
"type": "kernel",
"key": "KER ",
"keyColor": "32",
},
{
"type": "packages",
"format": "{} (pacman)",
"key": "PKG ",
"keyColor": "33",
},
{
"type": "shell",
"key": "SH ",
"keyColor": "34",
},
{
"type": "terminal",
"key": "TER ",
"keyColor": "35",
},
{
"type": "wm",
"key": "WM ",
"keyColor": "36",
},
"break",
]
}

View file

@ -0,0 +1,16 @@
.d8 .8uI
88R m888
88R %d88"
88R 88R
88R 88R
88R 88R
88R 88R
88R 88R
88R 88R
88B 88B
888 88*
.88u
%88*
8I*
J"*

View file

@ -0,0 +1,7 @@
.d8 .8u
88R 888
88R d8"
88R 88R
88B 88*
.88u 8I*
88R *IL

28
home/.config/git/config Normal file
View file

@ -0,0 +1,28 @@
# Sakura Line - git defaults. Your name, email and credentials stay in
# ~/.gitconfig, which git reads after this file, so it always wins.
[init]
defaultBranch = main
[pull]
rebase = true
[push]
autoSetupRemote = true
[diff]
algorithm = histogram
colorMoved = plain
mnemonicPrefix = true
[commit]
verbose = true
[column]
ui = auto
[branch]
sort = -committerdate
[tag]
sort = -version:refname
[rerere]
enabled = true
autoupdate = true
[alias]
co = checkout
br = branch
ci = commit
st = status

View file

@ -0,0 +1,11 @@
# Sakura Line - kitty. Font and colours come from the theme (rice-theme).
font_size 11.0
hide_window_decorations yes
confirm_os_window_close 0
window_padding_width 10
enable_audio_bell no
cursor_shape block
cursor_blink_interval 0
scrollback_lines 10000
include ~/.local/state/rice/theme/kitty.conf

View file

@ -0,0 +1,33 @@
# particles - wallpaper layer
# Tuned for bspwm/wallpaper/wallpaper.jpg: a bright, busy image, so the field
# is white and fairly opaque. Preview any change without touching the desktop:
# particles --snapshot /tmp/p.png --snapshot-bg ~/.config/bspwm/wallpaper/wallpaper.png
# Reload a running instance after editing: pkill -HUP particles
density = 55
radius = 2.4
glow = 4.0
opacity = 1.0
color = ffffff
link-distance = 140
link-width = 1.6
link-opacity = 0.85
link-color = ffffff
speed = 24
edges = bounce
cursor = grab,repel
cursor-distance = 180
cursor-strength = 1.0
cursor-response = 9
fps = 60
keep-below = true
pause-when-covered = true
mode = jigsaw
jigsaw-cols = 800
jigsaw-rows = 500
jigsaw-loose = 5

View file

@ -0,0 +1,60 @@
# Sakura Line - picom 13.
# One motion language for everything: windows, rofi, eww popups and dunst
# all appear with the same short scale-and-fade.
backend = "glx";
vsync = true;
use-damage = true;
detect-client-opacity = true;
detect-transient = true;
unredir-if-possible = true;
log-level = "warn";
shadow = false;
blur-background = false;
fading = true;
fade-in-step = 0.08;
fade-out-step = 0.08;
fade-delta = 8;
corner-radius = 12;
animations = (
{ triggers = [ "open", "show" ]; preset = "appear"; scale = 0.94; duration = 0.16; },
{ triggers = [ "close", "hide" ]; preset = "disappear"; scale = 0.96; duration = 0.12; }
);
rules = (
# The bar and desktop layer draw their own shape and never animate.
{ match = "window_type = 'dock' || window_type = 'desktop'"; corner-radius = 0; fade = false; animations = (); },
# Fullscreen video and games: square, and let them unredirect.
{ match = "fullscreen"; corner-radius = 0; },
# Toolkit menus and tooltips pop instantly.
{ match = "window_type = 'tooltip' || window_type = 'popup_menu' || window_type = 'dropdown_menu' || window_type = 'menu'"; corner-radius = 0; animations = (); },
# dunst rounds its own corners; picom rounding on top leaves a halo.
{ match = "class_g = 'Dunst'"; corner-radius = 0; },
# Menus and popups share a quicker, flatter version of the window motion.
{ match = "class_g = 'Rofi' || class_g ~= '^eww'";
animations = (
{ triggers = [ "open", "show" ]; preset = "appear"; scale = 0.98; duration = 0.10; },
{ triggers = [ "close", "hide" ]; preset = "disappear"; scale = 0.98; duration = 0.08; }
);
},
# The drop-down terminal slides from the top edge.
{ match = "class_g = 'rice-scratchpad'";
animations = (
{ triggers = [ "open", "show" ]; preset = "slide-in"; direction = "down"; duration = 0.18; },
{ triggers = [ "close", "hide" ]; preset = "slide-out"; direction = "up"; duration = 0.14; }
);
},
# Capture overlays must not move or fade while you aim.
{ match = "class_g = 'flameshot' || class_g = 'boomer' || class_g = 'slop'"; corner-radius = 0; fade = false; animations = (); },
# The webcam overlay stays a clean rounded tile.
{ match = "class_i = 'rice-webcam'"; corner-radius = 16; }
);

View file

@ -0,0 +1,121 @@
; Sakura Line - polybar. Colours come from the rendered theme.
; Every label opens its panel: left click = panel, right click = quick action.
include-file = ~/.local/state/rice/theme/polybar-colors.ini
include-file = ~/.local/state/rice/theme/polybar-modules.ini
[bar/top]
monitor = ${env:MONITOR:}
wm-restack = bspwm
enable-ipc = true
; Edges line up with tiled windows: bspwm draws borders inside the gap, so their
; outer edges sit one gap (10px) in from each side, so 100% minus 20px.
width = 100%:-20
height = 28
radius = 10
offset-x = 10
offset-y = 5
fixed-center = true
background = ${colors.bg-trans}
foreground = ${colors.text}
border-size = 2
border-color = ${colors.border}
padding = 1
module-margin = 0
cursor-click = pointer
cursor-scroll = ns-resize
font-0 = ${fonts.main}
font-1 = ${fonts.large}
; The clock sits alone in the centre so nothing that comes and goes (the
; player, recording or reminder indicators) can push it off centre.
modules-left = menu bspwm media
modules-center = date
modules-right = indicators cpu sep ram sep gpu sep net sep bt sep vol sep notif
[module/sep]
type = custom/text
format = "|"
format-foreground = ${colors.dim}
format-padding = 0
[module/menu]
type = custom/text
format = "󰣇"
format-foreground = ${colors.accent}
format-font = 2
format-padding = 1
click-left = rice-menu main
click-right = rice-panel hub
[module/bspwm]
type = internal/bspwm
pin-workspaces = true
inline-mode = true
enable-click = true
enable-scroll = true
format = <label-state>
label-focused = "[%name%]"
label-focused-foreground = ${colors.accent}
label-focused-padding = 1
label-occupied = "%name%"
label-occupied-foreground = ${colors.text}
label-occupied-padding = 1
label-empty = "%name%"
label-empty-foreground = ${colors.dim}
label-empty-padding = 1
label-urgent = "!%name%!"
label-urgent-foreground = ${colors.urgent}
label-urgent-padding = 1
[module/indicators]
type = custom/script
exec = rice-status indicators
tail = true
format-padding = 1
[module/media]
type = custom/script
exec = rice-status media
tail = true
format-padding = 1
click-left = rice-panel media
click-right = playerctl play-pause
scroll-up = playerctl previous
scroll-down = playerctl next
[module/net]
type = custom/script
exec = rice-status net
tail = true
format-padding = 1
click-left = rice-panel network
[module/bt]
type = custom/script
exec = rice-status bt
tail = true
format-padding = 1
click-left = rice-panel bluetooth
click-right = rice-bt power toggle
[module/vol]
type = custom/script
exec = rice-status vol
tail = true
format-padding = 1
click-left = rice-panel audio
click-right = rice-volume mute
scroll-up = rice-volume up
scroll-down = rice-volume down
[module/notif]
type = custom/script
exec = rice-status notif
tail = true
format-padding = 1
click-left = rice-panel notifications
click-middle = rice-notify dnd toggle
click-right = rice-notify dismiss-all
[settings]
screenchange-reload = true

View file

@ -0,0 +1,22 @@
# Sakura Line - aliases. Personal ones belong in ~/.bashrc.local.
if command -v eza >/dev/null; then
alias ls='eza -lh --group-directories-first --icons=auto'
alias lsa='ls -a'
alias lt='eza --tree --level=2 --long --icons --git'
alias lta='lt -a'
fi
alias ..='cd ..'
alias ...='cd ../..'
alias ....='cd ../../..'
alias g='git'
alias gs='git status -sb'
alias gcm='git commit -m'
alias gcam='git commit -a -m'
alias gcad='git commit -a --amend'
alias d='docker'
alias t='tmux attach || tmux new -s work'
alias decompress='tar -xzf'

View file

@ -0,0 +1,18 @@
# Sakura Line - everyday functions.
# ff: fuzzy-find a file below the current directory, with a preview (pictures
# show inline in st and kitty). eff opens the pick in $EDITOR.
ff() { fzf --preview 'rice-preview {}' --preview-window 'right,55%' "$@"; }
eff() { local f; f=$(ff) && [[ -n $f ]] && "$EDITOR" "$f"; }
# img <picture>...: show pictures right in the terminal.
img() { local f; for f in "$@"; do rice-preview "$f"; done; }
# open <file|url>: open with the default app, detached from the terminal.
open() { xdg-open "$@" >/dev/null 2>&1 & disown; }
# n: nvim, on the current directory when given nothing.
n() { if (( $# == 0 )); then nvim .; else nvim "$@"; fi; }
# compress <file|dir>: make <name>.tar.gz next to it (decompress is tar -xzf).
compress() { tar -czf "${1%/}.tar.gz" "${1%/}"; }

View file

@ -0,0 +1,24 @@
# Sakura Line - git worktree helpers.
# ga <branch>: new branch in its own worktree next to the repository
# (../<repo>-<branch>), and cd into it.
ga() {
local branch="${1:-}" top main dir
[[ -n $branch ]] || { echo "usage: ga <branch>"; return 1; }
top=$(git rev-parse --show-toplevel 2>/dev/null) || { echo "ga: not inside a git repository"; return 1; }
main=$(dirname "$(git -C "$top" rev-parse --path-format=absolute --git-common-dir)")
dir="$(dirname "$main")/$(basename "$main")-${branch//\//-}"
git -C "$top" worktree add -b "$branch" "$dir" && cd "$dir"
}
# gd: remove the current worktree and its branch (asks first; never the main one).
gd() {
local top main branch answer
top=$(git rev-parse --show-toplevel 2>/dev/null) || { echo "gd: not inside a git repository"; return 1; }
main=$(git worktree list --porcelain | awk '/^worktree / { print $2; exit }')
[[ $top != "$main" ]] || { echo "gd: this is the main worktree; not removing it"; return 1; }
branch=$(git branch --show-current)
read -rp "Remove worktree $top and branch $branch? [y/N] " answer
[[ $answer == [yY]* ]] || return 1
cd "$main" && git worktree remove "$top" && git branch -D "$branch"
}

View file

@ -0,0 +1,34 @@
# Sakura Line - rsync watchers.
# rsw <source> <destination>: keep destination in sync with source in the
# background, re-syncing on every change. The destination can be remote
# (host:path); one SSH connection is reused, so you authenticate once.
rsw() {
(( $# == 2 )) || { echo "usage: rsw <source> <destination>"; return 1; }
local src="${1%/}" dest="$2" sockets="${XDG_RUNTIME_DIR:-$HOME/.ssh}"
local rsh="ssh -o ControlMaster=auto -o ControlPath=$sockets/rsw-%r@%h:%p -o ControlPersist=yes"
setsid --fork env RSYNC_RSH="$rsh" bash -c '
rsync -a "$1/" "$2"
while inotifywait -r -q -e modify,create,delete,move "$1" >/dev/null; do rsync -a "$1/" "$2"; done
' rsw-watch "$src" "$dest" >/dev/null 2>&1
echo "watching $src -> $dest"
}
# lsw: list watchers. dsw: stop them all.
lsw() {
local pid cmd rest found=0
while read -r pid cmd; do
rest="${cmd##*rsw-watch }"
echo "$pid: ${rest% *} -> ${rest##* }"
found=1
done < <(pgrep -af 'rsw-watch ')
(( found )) || echo "no active watchers"
}
dsw() {
local pid found=0
for pid in $(pgrep -f 'rsw-watch '); do
kill -- -"$pid" 2>/dev/null && echo "stopped $pid" && found=1
done
(( found )) || echo "no active watchers"
}

View file

@ -0,0 +1,82 @@
# Sakura Line - ssh helpers.
# ssh: when an interactive session drops, tidy the terminal and reconnect until
# you press Ctrl-C. Only a real dropped shell reconnects: a remote command,
# piped input, or a failure within the first 30 seconds (bad host, bad key)
# returns normally, so nothing is ever replayed by accident.
ssh() {
local rc started=$SECONDS
command ssh "$@"
rc=$?
[[ -t 1 ]] || return $rc
_ssh_disarm
if (( rc != 255 )) || [[ ! -t 0 ]] || ! _ssh_interactive "$@" || (( SECONDS - started < 30 )); then
return $rc
fi
# A subshell, so Ctrl-C stops both the attempt and the loop.
(
while :; do
echo "Connection lost. Reconnecting (Ctrl-C to stop)..."
sleep 2
command ssh "$@"
rc=$?
_ssh_disarm
(( rc != 255 )) && exit $rc
done
)
}
# A connection that dies mid-session leaves the remote tmux or editor's
# terminal modes on (mouse reporting floods the prompt); switch them off.
_ssh_disarm() {
printf '\e[?1000l\e[?1002l\e[?1003l\e[?1006l\e[?1004l\e[?1049l\e[?25h'
}
# True when the arguments open a shell: a destination and no remote command,
# counting a RemoteCommand set in ssh_config.
_ssh_interactive() {
local value_opts="BbcDEeFIiJLlmOoPpQRSWw" argv=("$@") arg letters i dest="" opts_done=""
while (( $# )); do
arg="$1"; shift
if [[ -z $opts_done && $arg == "--" ]]; then
opts_done=1
elif [[ -z $opts_done && $arg == -?* ]]; then
letters="${arg#-}"
for (( i = 0; i < ${#letters}; i++ )); do
if [[ $value_opts == *"${letters:i:1}"* ]]; then
(( i == ${#letters} - 1 )) && shift
break
fi
done
elif [[ -z $dest ]]; then
dest="$arg"
else
return 1
fi
done
[[ -n $dest ]] || return 1
local resolved
resolved=$(command ssh -G "${argv[@]}" 2>/dev/null) || return 1
! grep -i '^remotecommand ' <<<"$resolved" | grep -qvi '^remotecommand none$'
}
# fip <host> <port>...: forward remote ports to localhost (web dev against a remote box).
fip() {
(( $# >= 2 )) || { echo "usage: fip <host> <port>..."; return 1; }
local host="$1" port; shift
for port in "$@"; do
command ssh -f -N -L "$port:localhost:$port" "$host" && echo "localhost:$port -> $host:$port"
done
}
# dip <port>...: stop those forwards. lip: list active forwards.
dip() {
(( $# >= 1 )) || { echo "usage: dip <port>..."; return 1; }
local port
for port in "$@"; do
pkill -f "ssh.*-L $port:localhost:$port" && echo "stopped $port" || echo "no forward on $port"
done
}
lip() { pgrep -af 'ssh.*-L [0-9]+:localhost:[0-9]+' || echo "no active forwards"; }

View file

@ -0,0 +1,22 @@
/* Sakura Line - rofi defaults. Scripts pick a layout with -theme. */
configuration {
modes: "drun,run,window";
show-icons: true;
icon-theme: "Papirus-Dark";
drun-display-format: "{name}";
matching: "fuzzy";
sort: true;
sorting-method: "fzf";
hover-select: true;
me-select-entry: "MousePrimary";
me-accept-entry: "!MousePrimary";
kb-cancel: "Escape,Control+g,Control+bracketleft";
/* Header titles, one per mode (rofi 2 ignores -display-<mode> for some modes). */
drun { display-name: "Apps"; }
run { display-name: "Run"; }
window { display-name: "Windows"; }
emoji { display-name: "Emoji"; }
}
@theme "~/.config/rofi/launcher.rasi"

View file

@ -0,0 +1,36 @@
/* Image grid: wallpaper and theme pickers. */
@import "~/.config/rofi/rice.rasi"
window {
width: 980px;
}
listview {
columns: 4;
lines: 2;
fixed-height: true;
flow: horizontal;
padding: 14px;
spacing: 10px;
}
element {
orientation: vertical;
padding: 10px;
spacing: 8px;
children: [ element-icon, element-text ];
}
element-icon {
size: 190px;
horizontal-align: 0.5;
}
element-text {
horizontal-align: 0.5;
text-color: @dim;
}
element selected.normal element-text {
text-color: @fg;
}

View file

@ -0,0 +1,6 @@
/* App launcher (super + w). */
@import "~/.config/rofi/rice.rasi"
window { width: 620px; }
listview { lines: 9; fixed-height: true; }
element-icon { size: 1.6em; }

View file

@ -0,0 +1,7 @@
/* Text menus: system menu, power, keybindings, clipboard, toggles.
Entries carry their own Nerd Font glyph, so rofi's icon column is off. */
@import "~/.config/rofi/rice.rasi"
window { width: 520px; }
listview { lines: 12; }
element-icon { enabled: false; }

View file

@ -0,0 +1,17 @@
/* Single text input: reminder text, event title, Wi-Fi password. */
@import "~/.config/rofi/rice.rasi"
window { width: 520px; }
mainbox { children: [ inputbar, message ]; }
inputbar {
border: 0px;
}
entry {
placeholder: "";
}
message {
border: 1px 0px 0px 0px;
}

View file

@ -0,0 +1,92 @@
/* Sakura Line - shared rofi layout. Every menu imports this, then overrides
only size and position. Colours come from the rendered theme. */
@import "~/.local/state/rice/theme/rofi-colors.rasi"
* {
background-color: transparent;
text-color: @fg;
font: @font-main;
}
window {
transparency: "real";
location: center;
anchor: center;
width: 560px;
background-color: @bg-trans;
border: 1px;
border-color: @border-col;
border-radius: 12px;
}
mainbox {
spacing: 0px;
children: [ inputbar, message, listview ];
}
inputbar {
padding: 14px 18px;
spacing: 0px;
border: 0px 0px 1px 0px;
border-color: @border-col;
children: [ prompt, entry ];
}
prompt {
text-color: @fg;
padding: 0px 10px 0px 0px;
vertical-align: 0.5;
font: @font-bold;
}
entry {
placeholder: "Search";
placeholder-color: @dim;
cursor: text;
vertical-align: 0.5;
font: @font-entry;
}
message {
padding: 10px 18px;
border: 0px 0px 1px 0px;
border-color: @border-col;
}
textbox {
text-color: @dim;
markup: true;
}
listview {
lines: 10;
columns: 1;
fixed-height: false;
scrollbar: false;
padding: 8px;
spacing: 2px;
}
element {
padding: 8px 10px;
spacing: 10px;
border-radius: 8px;
}
element normal.active, element alternate.active { text-color: @accent; }
element normal.urgent, element alternate.urgent { text-color: @urgent; }
element selected.normal { background-color: @select-bg; text-color: @select-fg; }
element selected.active { background-color: @select-bg; text-color: @accent; }
element selected.urgent { background-color: @select-bg; text-color: @urgent; }
element-icon {
size: 1.3em;
vertical-align: 0.5;
}
element-text {
text-color: inherit;
vertical-align: 0.5;
}
mode-switcher { enabled: false; }

244
home/.config/sxhkd/sxhkdrc Normal file
View file

@ -0,0 +1,244 @@
# Sakura Line - keybindings.
# `rice-keys` builds the cheat sheet (super + k) from this file:
# a "## Section" line starts a group, a "# text" line right above a
# hotkey is its description. Keep that shape when adding bindings.
## Apps
# Terminal
super + Return
st
# Drop-down terminal (scratchpad)
super + grave
rice-scratchpad
# App launcher
super + w
rofi -show drun
# Rice menu
super + m
rice-menu main
# Keybinding cheat sheet
super + k
rice-keys
# Emoji picker
super + ctrl + e
rice-emoji
# Clipboard history
super + ctrl + v
rice-clipboard
## Panels
# Control centre
super + a
rice-panel hub
# Audio / network / bluetooth / display / power / media panel
super + ctrl + {a,w,b,d,p,m}
rice-panel {audio,network,bluetooth,display,power,media}
# Calendar and reminders
super + ctrl + c
rice-panel calendar
# Notification centre
super + n
rice-panel notifications
# Toggle the bar
super + b
rice-bar toggle
## Notifications
# Dismiss the newest notification
super + comma
rice-notify dismiss
# Dismiss all notifications
super + shift + comma
rice-notify dismiss-all
# Do not disturb on/off
super + ctrl + comma
rice-notify dnd toggle
# Run the newest notification's action
super + alt + comma
rice-notify action
# Bring back the last dismissed notification
super + shift + alt + comma
rice-notify restore
## Reminders
# New reminder
super + ctrl + r
rice-reminder new
# Show upcoming reminders
super + ctrl + alt + r
rice-reminder list
## Capture
# Screenshot a region (flameshot)
Print
rice-capture region
# Screenshot the whole screen
shift + Print
rice-capture screen
# Copy text from a region (OCR)
ctrl + Print
rice-capture text
# Start or stop screen recording
alt + Print
rice-capture record
# Pick a colour
super + shift + Print
rice-capture color
# Capture menu: QR, webcam overlay, transcode and the rest
super + Print
rice-menu capture
# Screenshot a region (no PrtSc key)
super + alt + p
rice-capture region
# Screenshot the whole screen (no PrtSc key)
super + alt + shift + p
rice-capture screen
# Copy text from a region (no PrtSc key)
super + alt + t
rice-capture text
# Start or stop screen recording (no PrtSc key)
super + alt + v
rice-capture record
# Pick a colour (no PrtSc key)
super + alt + c
rice-capture color
# Capture menu (no PrtSc key)
super + alt + s
rice-menu capture
# Zoom into the screen
super + ctrl + z
rice-zoom
# Dictation on/off
super + ctrl + x
rice-dictate toggle
## Clipboard
# Copy (works in terminals too)
super + @c
rice-clip copy
# Paste (works in terminals too)
super + @v
rice-clip paste
## Style
# Wallpaper picker
super + ctrl + space
rice-menu wallpaper
# Theme picker
super + shift + ctrl + space
rice-menu theme
# Quick toggles
super + ctrl + o
rice-menu toggles
## Session
# Power menu
super + Escape
rice-menu power
# Lock
super + ctrl + l
rice-lock
# Stay awake on/off
super + ctrl + i
rice-idle toggle
# Reload keybindings
super + alt + Escape
pkill -USR1 -x sxhkd && rice-notify send "Keybindings" "Reloaded"
# Restart bspwm
super + alt + r
bspc wm -r
# Quit bspwm
super + Delete
bspc quit
## Windows
# Close window
super + q
bspc node -c
# Float / tile
super + space
bspc node -t "~"{floating,tiled}
# Fullscreen
super + f
bspc node -t \~fullscreen
# Focus / swap window in a direction
super + {_,shift + }{Left,Down,Up,Right}
bspc node -{f,s} {west,south,north,east}
# Previous window
alt + Tab
bspc node -f last
# Go to desktop / send window to desktop
super + {_,shift + }{1-6}
bspc {desktop -f,node -d} '^{1-6}'
# Previous / next desktop
super + ctrl + {Left,Right}
bspc desktop -f {prev,next}.local
## Media
# Volume up / down / mute
XF86Audio{RaiseVolume,LowerVolume,Mute}
rice-volume {up,down,mute}
# Microphone mute
XF86AudioMicMute
rice-volume mic-mute
# Play-pause / next / previous / stop
XF86Audio{Play,Pause,Next,Prev,Stop}
playerctl {play-pause,play-pause,next,previous,stop}
# Play-pause / next / previous without media keys
ctrl + alt + {c,x,z}
playerctl {play-pause,next,previous}

24
home/.inputrc Normal file
View file

@ -0,0 +1,24 @@
# Sakura Line - readline.
$include /etc/inputrc
set completion-ignore-case on
set completion-prefix-display-length 2
set show-all-if-ambiguous on
set show-all-if-unmodified on
set menu-complete-display-prefix on
set mark-symlinked-directories on
set match-hidden-files off
set page-completions off
set completion-query-items 200
set visible-stats on
set colored-stats on
set colored-completion-prefix on
set skip-completed-text on
# Up and Down search the history for what is already typed.
"\e[A": history-search-backward
"\e[B": history-search-forward
# Tab cycles through completions; Shift+Tab goes back.
TAB: menu-complete
"\e[Z": menu-complete-backward

11
home/.xinitrc Normal file
View file

@ -0,0 +1,11 @@
#!/bin/sh
# Sakura Line - X session: pick up the system's session setup (D-Bus and the
# systemd user environment), then hand over to bspwm.
if [ -d /etc/X11/xinit/xinitrc.d ]; then
for f in /etc/X11/xinit/xinitrc.d/?*.sh; do
[ -x "$f" ] && . "$f"
done
unset f
fi
exec bspwm

285
install.sh Executable file
View file

@ -0,0 +1,285 @@
#!/usr/bin/env bash
# Sakura Line - install the rice on Arch Linux.
#
# curl -fsSL https://rice.srtk.in/install.sh | bash
# ~/rice/install.sh full install (safe to run again)
# ~/rice/install.sh --update pull, re-link, re-render, rebuild what changed; asks nothing
# ~/rice/install.sh --dry-run show what would happen, change nothing
# ~/rice/install.sh --skip-packages everything is installed already; no pacman
#
# Installs the packages (pacman, plus paru for the AUR), links every file in
# home/ into $HOME and every script in bin/ into ~/.local/bin, renders the
# theme, builds st with sixel images, and offers calendar sync and a boot
# splash. Anything it would replace is moved to ~/.rice-backup-<date>/ first.
set -uo pipefail
RICE_REMOTE="${RICE_REMOTE:-https://git.srtk.in/sarthak/dotfiles.git}"
RICE_HOME="${RICE_HOME:-$HOME/rice}"
STATE="${XDG_STATE_HOME:-$HOME/.local/state}/rice"
BACKUP="$HOME/.rice-backup-$(date +%Y%m%d-%H%M%S)"
DRY=0
UPDATE=0
SKIP_PACKAGES=0
FAILED=()
for arg in "$@"; do
case "$arg" in
--update) UPDATE=1 ;;
--dry-run|-n) DRY=1 ;;
--skip-packages) SKIP_PACKAGES=1 ;;
-h|--help) sed -n '2,14p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) echo "unknown option: $arg (try --help)"; exit 2 ;;
esac
done
c_hd=$'\e[1;38;2;146;185;234m'; c_ok=$'\e[38;2;143;174;116m'; c_wa=$'\e[38;2;216;168;124m'
c_er=$'\e[38;2;207;106;128m'; c_dim=$'\e[38;2;127;119;150m'; c_off=$'\e[0m'
say() { printf '\n%s>>%s %s\n' "$c_hd" "$c_off" "$*"; }
ok() { printf ' %s+%s %s\n' "$c_ok" "$c_off" "$*"; }
warn() { printf ' %s!%s %s\n' "$c_wa" "$c_off" "$*"; }
fail() { printf ' %sx%s %s\n' "$c_er" "$c_off" "$*"; FAILED+=("$*"); }
run() { if (( DRY )); then printf ' %s$ %s%s\n' "$c_dim" "$*" "$c_off"; else "$@"; fi; }
ask() { (( UPDATE )) && return 1; local a=""; { read -rp " $1 [y/N] " a </dev/tty; } 2>/dev/null || return 1; [[ $a == [yY]* ]]; }
(( $(id -u) != 0 )) || { echo "Run this as your normal user; it asks for sudo when it needs it."; exit 1; }
command -v pacman >/dev/null || { echo "This rice installs on Arch Linux (pacman not found)."; exit 1; }
# ------------------------------------------------------------ find the repo
# Piped from curl there is no checkout yet: clone it, then run its own copy.
self_dir="$(cd "$(dirname "${BASH_SOURCE[0]:-.}")" 2>/dev/null && pwd)"
if [[ -f $self_dir/packages/pacman.txt && -d $self_dir/home ]]; then
RICE_DIR="$self_dir"
else
command -v git >/dev/null || sudo pacman -S --needed --noconfirm git
if [[ -d $RICE_HOME/.git ]]; then
git -C "$RICE_HOME" pull --ff-only
else
git clone "$RICE_REMOTE" "$RICE_HOME"
fi
exec bash "$RICE_HOME/install.sh" "$@" </dev/tty
fi
export PATH="$HOME/.local/bin:$PATH"
# ---------------------------------------------------------------- steps
update_repo() {
(( UPDATE )) || return 0
say "updating the rice"
if [[ -n $(git -C "$RICE_DIR" status --porcelain) ]]; then
warn "you have local changes in $RICE_DIR, so it was not pulled"
else
run git -C "$RICE_DIR" pull --ff-only || fail "git pull"
fi
}
packages() {
say "packages"
local repo aur helper tmp
mapfile -t repo < <(sed 's/#.*//' "$RICE_DIR/packages/pacman.txt" | awk 'NF { print $1 }')
mapfile -t aur < <(sed 's/#.*//' "$RICE_DIR/packages/aur.txt" | awk 'NF { print $1 }')
run sudo pacman -Syu --needed --noconfirm "${repo[@]}" || fail "pacman could not install everything"
helper=$(command -v paru || command -v yay || true)
if [[ -z $helper ]]; then
warn "no AUR helper; building paru"
tmp=$(mktemp -d)
if run git clone --depth 1 https://aur.archlinux.org/paru-bin.git "$tmp/paru-bin" &&
(cd "$tmp/paru-bin" && run makepkg -si --noconfirm); then
helper=$(command -v paru || true)
else
fail "could not build paru; AUR packages skipped"
fi
rm -rf "$tmp"
fi
[[ -n $helper ]] && { run "$helper" -S --needed --noconfirm "${aur[@]}" || fail "some AUR packages failed"; }
ok "packages done"
}
# link_one <source> <target>: symlink, moving whatever was there into the backup.
link_one() {
local src="$1" dst="$2" rel
[[ -L $dst && $(readlink "$dst") == "$src" ]] && return 0
if [[ -e $dst || -L $dst ]]; then
rel="${dst#"$HOME"/}"
run mkdir -p "$BACKUP/$(dirname "$rel")"
run mv "$dst" "$BACKUP/$rel"
warn "moved your ~/$rel to the backup"
fi
run mkdir -p "$(dirname "$dst")"
run ln -s "$src" "$dst"
}
# A previous dotfiles setup kept its own installer in ~/.config/rice, which is
# where this rice keeps your hooks and local settings. Move those files aside.
legacy_rice() {
local old="$HOME/.config/rice" f moved=0
for f in install.sh README.md packages.txt screenshot.webp particles st; do
[[ -e $old/$f && ! -L $old/$f ]] || continue
run mkdir -p "$BACKUP/.config/rice"
run mv "$old/$f" "$BACKUP/.config/rice/$f"
moved=1
done
(( moved )) && warn "moved the previous rice installer out of ~/.config/rice into the backup"
return 0
}
# Swap the running session over: stop the old bar, notifications, compositor
# and key daemon, start the rice's, and re-run bspwmrc. Windows stay open.
apply_now() {
[[ -n ${DISPLAY:-} ]] && pgrep -x bspwm >/dev/null || return 1
ask "Apply it to this session now? The bar, notifications, picom and sxhkd restart; windows stay open." || return 1
say "applying to this session"
run rice-session restart && run bspc wm -r && ok "applied" || fail "could not restart the session daemons"
}
link_home() {
say "linking configs and scripts"
local f t="$STATE/theme"
# bspwm runs bspwmrc as a program; without the executable bit it silently
# starts with no settings, rules or session.
run chmod +x "$RICE_DIR/home/.config/bspwm/bspwmrc" "$RICE_DIR"/bin/*
while IFS= read -r -d '' f; do
link_one "$f" "$HOME/${f#"$RICE_DIR"/home/}"
done < <(find "$RICE_DIR/home" \( -type f -o -type l \) -print0)
for f in "$RICE_DIR"/bin/*; do
link_one "$f" "$HOME/.local/bin/$(basename "$f")"
done
# These point at the rendered theme, which rice-theme rewrites on every change.
link_one "$t/dunst.conf" "$HOME/.config/dunst/dunstrc.d/50-theme.conf"
link_one "$t/eww-colors.scss" "$HOME/.config/eww/colors.scss"
link_one "$t/gtk.css" "$HOME/.config/gtk-3.0/gtk.css"
link_one "$t/gtk.css" "$HOME/.config/gtk-4.0/gtk.css"
link_one "$t/btop.theme" "$HOME/.config/btop/themes/rice.theme"
link_one "$t/imv-config" "$HOME/.config/imv/config"
[[ -d $BACKUP ]] && warn "replaced files are in $BACKUP"
ok "linked"
}
local_files() {
say "your own settings"
local hooks="$HOME/.config/rice/hooks" e
for e in post-boot theme-set font-set wallpaper-set lock unlock update; do run mkdir -p "$hooks/$e.d"; done
if [[ ! -f $HOME/.bashrc.local ]] && (( ! DRY )); then
printf '%s\n' "# Personal shell settings: aliases, exports, SDK paths. Not part of the rice repo." > "$HOME/.bashrc.local"
ok "created ~/.bashrc.local"
fi
if [[ ! -f $HOME/.config/rice/local.sh ]] && (( ! DRY )); then
printf '%s\n' "# Machine-specific bspwm extras, sourced by bspwmrc: apps to autostart," \
"# per-app desktop rules, monitor layouts. Not part of the rice repo." > "$HOME/.config/rice/local.sh"
ok "created ~/.config/rice/local.sh"
fi
ok "hooks live in ~/.config/rice/hooks/<event>.d/"
}
theme() {
say "theme"
local name; name=$(rice-theme current)
if [[ -n ${DISPLAY:-} ]] && pgrep -x bspwm >/dev/null; then
run rice-theme reload && ok "re-rendered $name and reloaded running apps" || fail "theme reload"
else
run rice-theme render "$name" && ok "rendered $name" || fail "theme render"
fi
}
build_st() {
say "st, with sixel images"
local stamp="$STATE/st.built" want
want=$(cat "$RICE_DIR"/st/build.sh "$RICE_DIR"/st/patches.list "$RICE_DIR"/st/config.sed | sha1sum | cut -c1-16)
if [[ -f $stamp && $(cat "$stamp") == "$want" ]] && command -v st >/dev/null; then
ok "st is up to date"; return
fi
if run bash "$RICE_DIR/st/build.sh" --install; then
(( DRY )) || { mkdir -p "$STATE"; echo "$want" > "$stamp"; }
ok "st installed"
else
fail "st build"
fi
}
build_particles() {
say "particles wallpaper layer"
if run make -s -C "$RICE_DIR/particles" && run sudo make -s -C "$RICE_DIR/particles" install; then
ok "particles installed"
else
fail "particles build"
fi
}
system_setup() {
say "services and hardware"
run sudo systemctl enable --now NetworkManager.service bluetooth.service power-profiles-daemon.service ||
warn "some services did not start (normal without that hardware)"
if [[ ! -f /etc/modules-load.d/i2c-dev.conf ]]; then
run sudo sh -c 'echo i2c-dev > /etc/modules-load.d/i2c-dev.conf' && run sudo modprobe i2c-dev &&
ok "i2c-dev loads at boot (monitor brightness over DDC)"
fi
if getent group i2c >/dev/null && ! id -nG | grep -qw i2c; then
run sudo usermod -aG i2c "$USER" && warn "added you to the i2c group; it applies at your next login"
fi
}
calendar() {
say "calendar"
if [[ -f ${XDG_CONFIG_HOME:-$HOME/.config}/khal/config ]]; then ok "already set up"; return; fi
if ask "Sync the calendar with a CalDAV server (Baikal, Nextcloud, Fastmail, ...)?"; then
run rice-cal setup || fail "calendar setup"
else
warn "skipped; run 'rice-cal setup' whenever you like"
fi
}
boot_splash() {
say "boot splash"
if grep -Eq '^HOOKS=.*\bplymouth\b' /etc/mkinitcpio.conf 2>/dev/null; then ok "plymouth is already in the initramfs"; return; fi
ask "Show Plymouth's stock splash at boot? This edits mkinitcpio.conf and the kernel options." || { warn "skipped"; return; }
run sudo cp /etc/mkinitcpio.conf /etc/mkinitcpio.conf.rice-backup
run sudo sed -i -E '/^HOOKS=/ s/\b(systemd|udev)\b/\1 plymouth/' /etc/mkinitcpio.conf
if [[ -f /etc/default/grub ]]; then
run sudo cp /etc/default/grub /etc/default/grub.rice-backup
grep -q 'splash' /etc/default/grub ||
run sudo sed -i -E 's/^(GRUB_CMDLINE_LINUX_DEFAULT="[^"]*)"/\1 quiet splash"/' /etc/default/grub
run sudo grub-mkconfig -o /boot/grub/grub.cfg >/dev/null || fail "grub-mkconfig"
elif compgen -G "/boot/loader/entries/*.conf" >/dev/null; then
local entry
for entry in /boot/loader/entries/*.conf; do
grep -q splash "$entry" || run sudo sed -i -E '/^options / s/$/ quiet splash/' "$entry"
done
else
warn "add 'quiet splash' to your kernel command line by hand"
fi
run sudo mkinitcpio -P >/dev/null && ok "plymouth enabled from the next boot" || fail "mkinitcpio"
}
# ----------------------------------------------------------------- main
printf '\n%s Sakura Line%s rice installer · %s\n' "$c_hd" "$c_off" "$RICE_DIR"
(( DRY )) && warn "dry run: nothing will be changed"
update_repo
(( SKIP_PACKAGES )) || packages
legacy_rice
link_home
local_files
theme
build_st
build_particles
system_setup
calendar
boot_splash
(( UPDATE )) && run rice-hook update
printf '\n'
if (( ${#FAILED[@]} == 0 )); then
say "done"
else
say "finished with ${#FAILED[@]} problem(s):"
for f in "${FAILED[@]}"; do printf ' %s- %s%s\n' "$c_er" "$f" "$c_off"; done
fi
if apply_now; then
printf '\n %ssuper + k%s lists every shortcut.\n\n' "$c_hd" "$c_off"
elif [[ -n ${DISPLAY:-} ]] && pgrep -x bspwm >/dev/null; then
printf ' To switch this session over later: %srice-session restart && bspc wm -r%s\n\n' "$c_hd" "$c_off"
else
printf ' Log in on tty1 and the desktop starts; %ssuper + k%s lists every shortcut.\n\n' "$c_hd" "$c_off"
fi

14
packages/aur.txt Normal file
View file

@ -0,0 +1,14 @@
# Sakura Line - AUR packages (installed with paru or yay).
# One package per line; text after # is ignored.
eww # popups: calendar, control panels, notification centre
xidlehook # idle chain that skips fullscreen video and playing audio
tdrop # drop-down scratchpad terminal
boomer-git # screen zoom
rofi-greenclip # clipboard history
zscroll-git # scrolling media title
nerd-dictation-git # offline voice dictation (vosk)
bibata-cursor-theme
cbonsai
pipes.sh
tty-clock

126
packages/pacman.txt Normal file
View file

@ -0,0 +1,126 @@
# Sakura Line - official repo packages (pacman -S --needed).
# One package per line; text after # is ignored.
# --- X session / window manager ---
xorg-server
xorg-xinit
xorg-xrandr
xorg-xprop
xorg-xrdb
xorg-xset
xorg-xsetroot
xorg-xwininfo
bspwm
sxhkd
polybar
picom
rofi
rofi-emoji
dunst
libnotify
feh
xdotool
xclip
xsettingsd
wmname
polkit-gnome
xdg-utils
# --- build toolchain (st, particles) ---
base-devel
git
pkgconf
libx11
libxext
libxft
libxrandr
libxfixes
libxinerama
cairo
harfbuzz
fontconfig
freetype2
gd
imlib2
# --- fonts / theming ---
ttf-jetbrains-mono-nerd
noto-fonts
noto-fonts-cjk
noto-fonts-emoji
papirus-icon-theme
gnome-themes-extra
qt6ct
# --- panels: audio, network, bluetooth, display, power, media ---
pipewire
pipewire-pulse
wireplumber
pamixer
playerctl
networkmanager
bluez
bluez-utils
ddcutil
power-profiles-daemon
lm_sensors
qrencode
# --- calendar + reminders (Baikal CalDAV) ---
khal
vdirsyncer
# --- idle, screensaver, lock ---
xss-lock
xsecurelock
xscreensaver
# --- capture: screenshot, recording, OCR, QR, colour, transcode ---
flameshot
maim
slop
gpu-screen-recorder
ffmpeg
imagemagick
mpv
v4l-utils
tesseract
tesseract-data-eng
zbar
xcolor
# --- shell ---
bash-completion
starship
fzf
eza
bat
zoxide
fd
ripgrep
tldr
jq
inotify-tools
socat
# --- terminal images ---
chafa
ueberzugpp
yazi
imv
# --- boot splash (stock Arch theme, no branding) ---
plymouth
# --- apps ---
kitty
neovim
thunar
fastfetch
btop
cava
# --- terminal toys ---
cmatrix
asciiquarium
onefetch

21
particles/LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

23
particles/Makefile Normal file
View file

@ -0,0 +1,23 @@
PREFIX ?= /usr/local
BINDIR ?= $(PREFIX)/bin
PKGS := cairo x11 xext xfixes xrender xrandr
CFLAGS ?= -O2 -Wall -Wextra
CFLAGS += $(shell pkg-config --cflags $(PKGS))
LDLIBS := $(shell pkg-config --libs $(PKGS)) -lm
all: particles
particles: particles_wall.c
$(CC) $(CFLAGS) -o $@ $< $(LDLIBS)
install: particles
install -Dm755 particles $(DESTDIR)$(BINDIR)/particles
uninstall:
rm -f $(DESTDIR)$(BINDIR)/particles
clean:
rm -f particles
.PHONY: all install uninstall clean

1744
particles/particles_wall.c Normal file

File diff suppressed because it is too large Load diff

61
st/build.sh Executable file
View file

@ -0,0 +1,61 @@
#!/usr/bin/env bash
# st/build.sh - build st from st-flexipatch with the rice's patches and settings.
#
# st/build.sh [--install] [--src DIR]
#
# Clones st-flexipatch (pinned below) into ~/.local/src/st-flexipatch, or reuses
# --src, turns on the patches in st/patches.list, applies st/config.sed to
# upstream's config.def.h, enables the libraries those patches need in
# config.mk, and runs make. --install then runs `sudo make install`.
set -euo pipefail
here="$(cd "$(dirname "$(readlink -f "$0")")" && pwd)"
repo="https://github.com/bakkeby/st-flexipatch"
commit="aa56259" # pinned: bump on purpose, after checking patches.list and config.sed still apply
src="${ST_SRC:-$HOME/.local/src/st-flexipatch}"
install=0
while (( $# )); do
case "$1" in
--install) install=1 ;;
--src) src="${2:?--src needs a directory}"; shift ;;
*) echo "usage: st/build.sh [--install] [--src DIR]" >&2; exit 2 ;;
esac
shift
done
if [[ ! -d $src/.git ]]; then
mkdir -p "$(dirname "$src")"
git clone --quiet "$repo" "$src"
fi
if [[ $(git -C "$src" rev-parse --is-shallow-repository) == true ]]; then
git -C "$src" fetch --quiet --unshallow origin
fi
git -C "$src" checkout --quiet --force "$commit"
cd "$src"
# patches.h: upstream defaults with our list switched on.
enabled=$(sed 's/#.*//' "$here/patches.list" | awk 'NF { print $1 }')
cp patches.def.h patches.h
for p in $enabled; do
grep -q "^#define ${p}_PATCH " patches.h || { echo "st/build.sh: no patch called ${p}_PATCH" >&2; exit 1; }
sed -i "s/^#define ${p}_PATCH 0$/#define ${p}_PATCH 1/" patches.h
done
# config.h: upstream defaults plus our values and shortcuts.
sed -f "$here/config.sed" config.def.h > config.h
# config.mk: link the libraries the enabled patches need.
has() { grep -qx "$1" <<<"$enabled"; }
has ALPHA && sed -i -E 's/^#(XRENDER = )/\1/' config.mk
has LIGATURES && sed -i -E 's/^#(LIGATURES_(C|H|INC|LIBS) = )/\1/' config.mk
has SIXEL && sed -i -E 's/^#(SIXEL_(C|LIBS) = )/\1/' config.mk
make clean >/dev/null
make -j"$(nproc)"
if (( install )); then
sudo make install
fi
echo "st built in $src"

24
st/config.sed Normal file
View file

@ -0,0 +1,24 @@
# st/config.sed - the rice's changes to st-flexipatch's config.def.h.
# The font and colours here are only fallbacks: rice-theme sets both through
# Xresources, and st re-reads them on USR1.
s|^static char \*font = .*|static char *font = "JetBrainsMono Nerd Font:pixelsize=14:antialias=true:autohint=true";|
s|^static int borderpx = .*|static int borderpx = 15;|
s|^float alpha = .*|float alpha = 0.75;|
s|^static unsigned int cursorshape = .*|static unsigned int cursorshape = 2;|
# Alt-based zoom, scrolling and transparency, as in the previous st build.
/^static Shortcut shortcuts\[\] = {/a\
{ MODKEY, XK_comma, zoom, {.f = +1} },\
{ MODKEY, XK_period, zoom, {.f = -1} },\
{ MODKEY, XK_g, zoomreset, {.f = 0} },\
{ MODKEY, XK_Page_Up, kscrollup, {.i = -1}, S_PRI },\
{ MODKEY, XK_Page_Down, kscrolldown, {.i = -1}, S_PRI },\
{ MODKEY, XK_k, kscrollup, {.i = 1}, S_PRI },\
{ MODKEY, XK_j, kscrolldown, {.i = 1}, S_PRI },\
{ MODKEY, XK_Up, kscrollup, {.i = 1}, S_PRI },\
{ MODKEY, XK_Down, kscrolldown, {.i = 1}, S_PRI },\
{ MODKEY, XK_u, kscrollup, {.i = -1}, S_PRI },\
{ MODKEY, XK_d, kscrolldown, {.i = -1}, S_PRI },\
{ MODKEY, XK_a, changealpha, {.f = +0.05} },\
{ MODKEY, XK_s, changealpha, {.f = -0.05} },

575
st/config.siduck.h Normal file
View file

@ -0,0 +1,575 @@
/* See LICENSE file for copyright and license details. */
/*
* appearance
*
* font: see http://freedesktop.org/software/fontconfig/fontconfig-user.html
*/
static char *font = "JetBrainsMono Nerd Font :pixelsize=14:antialias=true:autohint=true";
static char *font2[] = { "JetBrainsMono Nerd Font :pixelsize=14:antialias=true:autohint=true" };
static int borderpx = 15;
/*
* What program is execed by st depends of these precedence rules:
* 1: program passed with -e
* 2: utmp option
* 3: SHELL environment variable
* 4: value of shell in /etc/passwd
* 5: value of shell in config.h
*/
static char *shell = "/bin/sh";
char *utmp = NULL;
char *stty_args = "stty raw pass8 nl -echo -iexten -cstopb 38400";
/* identification sequence returned in DA and DECID */
char *vtiden = "\033[?6c";
/* Kerning / character bounding-box multipliers */
static float cwscale = 1.0;
static float chscale = 1.0;
/*
* word delimiter string
*
* More advanced example: L" `'\"()[]{}"
*/
wchar_t *worddelimiters = L" ";
/* selection timeouts (in milliseconds) */
static unsigned int doubleclicktimeout = 300;
static unsigned int tripleclicktimeout = 600;
/* alt screens */
int allowaltscreen = 1;
/*
* draw latency range in ms - from new content/keypress/etc until drawing.
* within this range, st draws when content stops arriving (idle). mostly it's
* near minlatency, but it waits longer for slow updates to avoid partial draw.
* low minlatency will tear/flicker more, as it can "detect" idle too early.
*/
static double minlatency = 8;
static double maxlatency = 33;
/*
* Synchronized-Update timeout in ms
* https://gitlab.com/gnachman/iterm2/-/wikis/synchronized-updates-spec
*/
static uint su_timeout = 200;
/*
* blinking timeout (set to 0 to disable blinking) for the terminal blinking
* attribute.
*/
static unsigned int blinktimeout = 800;
/*
* interval (in milliseconds) between each successive call to ximspot. This
* improves terminal performance while not reducing functionality to those
* whom need XIM support.
*/
int ximspot_update_interval = 1000;
/*
* thickness of underline and bar cursors
*/
static unsigned int cursorthickness = 2;
/*
* 1: render most of the lines/blocks characters without using the font for
* perfect alignment between cells (U2500 - U259F except dashes/diagonals).
* Bold affects lines thickness if boxdraw_bold is not 0. Italic is ignored.
* 0: disable (render all U25XX glyphs normally from the font).
*/
const int boxdraw = 1;
const int boxdraw_bold = 1;
/* braille (U28XX): 1: render as adjacent "pixels", 0: use font */
const int boxdraw_braille = 1;
/*
* bell volume. It must be a value between -100 and 100. Use 0 for disabling
* it
*/
static int bellvolume = 0;
/* default TERM value */
char *termname = "st-256color";
/*
* spaces per tab
*
* When you are changing this value, don't forget to adapt the »it« value in
* the st.info and appropriately install the st.info in the environment where
* you use this st version.
*
* it#$tabspaces,
*
* Secondly make sure your kernel is not expanding tabs. When running `stty
* -a` »tab0« should appear. You can tell the terminal to not expand tabs by
* running following command:
*
* stty tabs
*/
unsigned int tabspaces = 8;
/* bg opacity */
float alpha = 0.72;
/* Terminal colors (16 first used in escape sequence) */
static const char *colorname[] = {
/* Sakura Line - sampled from bspwm/wallpaper/wallpaper.jpg */
/* 8 normal colors */
"#0f0d17", /* black */
"#cf6a80", /* red */
"#8fae74", /* green */
"#d8a87c", /* yellow */
"#7c9ede", /* blue */
"#b483c0", /* magenta */
"#8ab6c4", /* cyan */
"#c9d1e2", /* white */
/* 8 bright colors */
"#3a3350", /* black */
"#e0798f", /* red */
"#a8c68d", /* green */
"#e9c79b", /* yellow */
"#92b9ea", /* blue */
"#d0a4d8", /* magenta */
"#a3c0cb", /* cyan */
"#e9eefb", /* white */
};
/*
* Default colors (colorname index)
* foreground, background, cursor, reverse cursor
*/
unsigned int defaultfg = 15; // #e9eefb
unsigned int defaultbg = 0; // #0f0d17
unsigned int defaultcs = 12; // cursor: #92b9ea (sky)
unsigned int defaultrcs = 0;
/*
* https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h4-Functions-using-CSI-_-ordered-by-the-final-character-lparen-s-rparen:CSI-Ps-SP-q.1D81
* Default style of cursor
* 0: Blinking block
* 1: Blinking block (default)
* 2: Steady block ("")
* 3: Blinking underline
* 4: Steady underline ("_")
* 5: Blinking bar
* 6: Steady bar ("|")
* 7: Blinking st cursor
* 8: Steady st cursor
*/
static unsigned int cursorshape = 1;
/*
* Default columns and rows numbers
*/
static unsigned int cols = 80;
static unsigned int rows = 24;
/*
* Default colour and shape of the mouse cursor
*/
static unsigned int mouseshape = XC_xterm;
static unsigned int mousefg = 7;
static unsigned int mousebg = 0;
/*
* Color used to display font attributes when fontconfig selected a font which
* doesn't match the ones requested.
*/
static unsigned int defaultattr = 11;
/*
* Xresources preferences to load at startup
*/
ResourcePref resources[] = {
{ "font", STRING, &font },
{ "fontalt0", STRING, &font2[0] },
{ "color0", STRING, &colorname[0] },
{ "color1", STRING, &colorname[1] },
{ "color2", STRING, &colorname[2] },
{ "color3", STRING, &colorname[3] },
{ "color4", STRING, &colorname[4] },
{ "color5", STRING, &colorname[5] },
{ "color6", STRING, &colorname[6] },
{ "color7", STRING, &colorname[7] },
{ "color8", STRING, &colorname[8] },
{ "color9", STRING, &colorname[9] },
{ "color10", STRING, &colorname[10] },
{ "color11", STRING, &colorname[11] },
{ "color12", STRING, &colorname[12] },
{ "color13", STRING, &colorname[13] },
{ "color14", STRING, &colorname[14] },
{ "color15", STRING, &colorname[15] },
{ "background", STRING, &colorname[258] },
{ "foreground", STRING, &colorname[259] },
{ "cursorColor", STRING, &colorname[256] },
{ "termname", STRING, &termname },
{ "shell", STRING, &shell },
{ "blinktimeout", INTEGER, &blinktimeout },
{ "bellvolume", INTEGER, &bellvolume },
{ "tabspaces", INTEGER, &tabspaces },
{ "borderpx", INTEGER, &borderpx },
{ "cwscale", FLOAT, &cwscale },
{ "chscale", FLOAT, &chscale },
{ "alpha", FLOAT, &alpha },
{ "ximspot_update_interval", INTEGER, &ximspot_update_interval },
};
/*
* Internal mouse shortcuts.
* Beware that overloading Button1 will disable the selection.
*/
const unsigned int mousescrollincrement = 3;
static MouseShortcut mshortcuts[] = {
/* button mask string */
{ Button4, XK_NO_MOD, "\031" },
{ Button5, XK_NO_MOD, "\005" },
};
/* Internal keyboard shortcuts. */
#define MODKEY Mod1Mask
#define TERMMOD (Mod4Mask|ShiftMask)
MouseKey mkeys[] = {
/* button mask function argument */
{ Button4, XK_NO_MOD, kscrollup, {.i = mousescrollincrement} },
{ Button5, XK_NO_MOD, kscrolldown, {.i = mousescrollincrement} },
{ Button4, Mod4Mask, zoom, {.f = +1} },
{ Button5, Mod4Mask, zoom, {.f = -1} },
};
static char *openurlcmd[] = { "/bin/sh", "-c", "st-urlhandler", "externalpipe", NULL };
static char *copyurlcmd[] = { "/bin/sh", "-c",
"tmp=$(sed 's/.*│//g' | tr -d '\n' | grep -aEo '(((http|https|gopher|gemini|ftp|ftps|git)://|www\\.)[a-zA-Z0-9.]*[:]?[a-zA-Z0-9./@$&%?$#=_-~]*)|((magnet:\\?xt=urn:btih:)[a-zA-Z0-9]*)' | uniq | sed 's/^www./http:\\/\\/www\\./g' ); IFS=; [ ! -z $tmp ] && echo $tmp | dmenu -i -p 'Copy which url?' -l 10 | tr -d '\n' | xclip -selection clipboard",
"externalpipe", NULL };
static char *copyoutput[] = { "/bin/sh", "-c", "st-copyout", "externalpipe", NULL };
static Shortcut shortcuts[] = {
/* mask keysym function argument */
{ XK_ANY_MOD, XK_Break, sendbreak, {.i = 0} },
{ ControlMask, XK_Print, toggleprinter, {.i = 0} },
{ ShiftMask, XK_Print, printscreen, {.i = 0} },
{ XK_ANY_MOD, XK_Print, printsel, {.i = 0} },
{ MODKEY, XK_comma, zoom, {.f = +1} },
{ MODKEY, XK_period, zoom, {.f = -1} },
{ MODKEY, XK_g, zoomreset, {.f = 0} },
{ ControlMask | ShiftMask, XK_C, clipcopy, {.i = 0} },
{ ShiftMask, XK_Insert, clippaste, {.i = 0} },
{ ControlMask | ShiftMask, XK_V, clippaste, {.i = 0} },
{ XK_ANY_MOD, Button2, selpaste, {.i = 0} },
{ MODKEY, XK_Num_Lock, numlock, {.i = 0} },
{ ControlMask | ShiftMask, XK_U, iso14755, {.i = 0} },
{ ShiftMask, XK_Page_Up, kscrollup, {.i = -1} },
{ ShiftMask, XK_Page_Down, kscrolldown, {.i = -1} },
{ MODKEY, XK_Page_Up, kscrollup, {.i = -1} },
{ MODKEY, XK_Page_Down, kscrolldown, {.i = -1} },
{ MODKEY, XK_k, kscrollup, {.i = 1} },
{ MODKEY, XK_j, kscrolldown, {.i = 1} },
{ MODKEY, XK_Up, kscrollup, {.i = 1} },
{ MODKEY, XK_Down, kscrolldown, {.i = 1} },
{ MODKEY, XK_u, kscrollup, {.i = -1} },
{ MODKEY, XK_d, kscrolldown, {.i = -1} },
{ MODKEY, XK_s, changealpha, {.f = -0.05} },
{ MODKEY, XK_a, changealpha, {.f = +0.05} },
{ MODKEY, XK_m, changealpha, {.f = +2.00} },
{ TERMMOD, XK_Up, zoom, {.f = +1} },
{ TERMMOD, XK_Down, zoom, {.f = -1} },
{ TERMMOD, XK_K, zoom, {.f = +1} },
{ TERMMOD, XK_J, zoom, {.f = -1} },
{ TERMMOD, XK_U, zoom, {.f = +2} },
{ TERMMOD, XK_D, zoom, {.f = -2} },
{ MODKEY, XK_l, externalpipe, {.v = openurlcmd } },
{ MODKEY, XK_y, externalpipe, {.v = copyurlcmd } },
{ MODKEY, XK_o, externalpipe, {.v = copyoutput } },
{ TERMMOD, XK_Return, newterm, {.i = 0} },
};
/*
* Special keys (change & recompile st.info accordingly)
*
* Mask value:
* * Use XK_ANY_MOD to match the key no matter modifiers state
* * Use XK_NO_MOD to match the key alone (no modifiers)
* appkey value:
* * 0: no value
* * > 0: keypad application mode enabled
* * = 2: term.numlock = 1
* * < 0: keypad application mode disabled
* appcursor value:
* * 0: no value
* * > 0: cursor application mode enabled
* * < 0: cursor application mode disabled
* crlf value
* * 0: no value
* * > 0: crlf mode is enabled
* * < 0: crlf mode is disabled
*
* Be careful with the order of the definitions because st searches in
* this table sequentially, so any XK_ANY_MOD must be in the last
* position for a key.
*/
/*
* If you want keys other than the X11 function keys (0xFD00 - 0xFFFF)
* to be mapped below, add them to this array.
*/
static KeySym mappedkeys[] = { -1 };
/*
* State bits to ignore when matching key or button events. By default,
* numlock (Mod2Mask) and keyboard layout (XK_SWITCH_MOD) are ignored.
*/
static uint ignoremod = Mod2Mask|XK_SWITCH_MOD;
/*
* Override mouse-select while mask is active (when MODE_MOUSE is set).
* Note that if you want to use ShiftMask with selmasks, set this to an other
* modifier, set to 0 to not use it.
*/
static uint forceselmod = ShiftMask;
/*
* This is the huge key array which defines all compatibility to the Linux
* world. Please decide about changes wisely.
*/
static Key key[] = {
/* keysym mask string appkey appcursor */
{ XK_KP_Home, ShiftMask, "\033[2J", 0, -1},
{ XK_KP_Home, ShiftMask, "\033[1;2H", 0, +1},
{ XK_KP_Home, XK_ANY_MOD, "\033[H", 0, -1},
{ XK_KP_Home, XK_ANY_MOD, "\033[1~", 0, +1},
{ XK_KP_Up, XK_ANY_MOD, "\033Ox", +1, 0},
{ XK_KP_Up, XK_ANY_MOD, "\033[A", 0, -1},
{ XK_KP_Up, XK_ANY_MOD, "\033OA", 0, +1},
{ XK_KP_Down, XK_ANY_MOD, "\033Or", +1, 0},
{ XK_KP_Down, XK_ANY_MOD, "\033[B", 0, -1},
{ XK_KP_Down, XK_ANY_MOD, "\033OB", 0, +1},
{ XK_KP_Left, XK_ANY_MOD, "\033Ot", +1, 0},
{ XK_KP_Left, XK_ANY_MOD, "\033[D", 0, -1},
{ XK_KP_Left, XK_ANY_MOD, "\033OD", 0, +1},
{ XK_KP_Right, XK_ANY_MOD, "\033Ov", +1, 0},
{ XK_KP_Right, XK_ANY_MOD, "\033[C", 0, -1},
{ XK_KP_Right, XK_ANY_MOD, "\033OC", 0, +1},
{ XK_KP_Prior, ShiftMask, "\033[5;2~", 0, 0},
{ XK_KP_Prior, XK_ANY_MOD, "\033[5~", 0, 0},
{ XK_KP_Begin, XK_ANY_MOD, "\033[E", 0, 0},
{ XK_KP_End, ControlMask, "\033[J", -1, 0},
{ XK_KP_End, ControlMask, "\033[1;5F", +1, 0},
{ XK_KP_End, ShiftMask, "\033[K", -1, 0},
{ XK_KP_End, ShiftMask, "\033[1;2F", +1, 0},
{ XK_KP_End, XK_ANY_MOD, "\033[4~", 0, 0},
{ XK_KP_Next, ShiftMask, "\033[6;2~", 0, 0},
{ XK_KP_Next, XK_ANY_MOD, "\033[6~", 0, 0},
{ XK_KP_Insert, ShiftMask, "\033[2;2~", +1, 0},
{ XK_KP_Insert, ShiftMask, "\033[4l", -1, 0},
{ XK_KP_Insert, ControlMask, "\033[L", -1, 0},
{ XK_KP_Insert, ControlMask, "\033[2;5~", +1, 0},
{ XK_KP_Insert, XK_ANY_MOD, "\033[4h", -1, 0},
{ XK_KP_Insert, XK_ANY_MOD, "\033[2~", +1, 0},
{ XK_KP_Delete, ControlMask, "\033[M", -1, 0},
{ XK_KP_Delete, ControlMask, "\033[3;5~", +1, 0},
{ XK_KP_Delete, ShiftMask, "\033[2K", -1, 0},
{ XK_KP_Delete, ShiftMask, "\033[3;2~", +1, 0},
{ XK_KP_Delete, XK_ANY_MOD, "\033[3~", -1, 0},
{ XK_KP_Delete, XK_ANY_MOD, "\033[3~", +1, 0},
{ XK_KP_Multiply, XK_ANY_MOD, "\033Oj", +2, 0},
{ XK_KP_Add, XK_ANY_MOD, "\033Ok", +2, 0},
{ XK_KP_Enter, XK_ANY_MOD, "\033OM", +2, 0},
{ XK_KP_Enter, XK_ANY_MOD, "\r", -1, 0},
{ XK_KP_Subtract, XK_ANY_MOD, "\033Om", +2, 0},
{ XK_KP_Decimal, XK_ANY_MOD, "\033On", +2, 0},
{ XK_KP_Divide, XK_ANY_MOD, "\033Oo", +2, 0},
{ XK_KP_0, XK_ANY_MOD, "\033Op", +2, 0},
{ XK_KP_1, XK_ANY_MOD, "\033Oq", +2, 0},
{ XK_KP_2, XK_ANY_MOD, "\033Or", +2, 0},
{ XK_KP_3, XK_ANY_MOD, "\033Os", +2, 0},
{ XK_KP_4, XK_ANY_MOD, "\033Ot", +2, 0},
{ XK_KP_5, XK_ANY_MOD, "\033Ou", +2, 0},
{ XK_KP_6, XK_ANY_MOD, "\033Ov", +2, 0},
{ XK_KP_7, XK_ANY_MOD, "\033Ow", +2, 0},
{ XK_KP_8, XK_ANY_MOD, "\033Ox", +2, 0},
{ XK_KP_9, XK_ANY_MOD, "\033Oy", +2, 0},
{ XK_Up, ShiftMask, "\033[1;2A", 0, 0},
{ XK_Up, Mod1Mask, "\033[1;3A", 0, 0},
{ XK_Up, ShiftMask|Mod1Mask,"\033[1;4A", 0, 0},
{ XK_Up, ControlMask, "\033[1;5A", 0, 0},
{ XK_Up, ShiftMask|ControlMask,"\033[1;6A", 0, 0},
{ XK_Up, ControlMask|Mod1Mask,"\033[1;7A", 0, 0},
{ XK_Up,ShiftMask|ControlMask|Mod1Mask,"\033[1;8A", 0, 0},
{ XK_Up, XK_ANY_MOD, "\033[A", 0, -1},
{ XK_Up, XK_ANY_MOD, "\033OA", 0, +1},
{ XK_Down, ShiftMask, "\033[1;2B", 0, 0},
{ XK_Down, Mod1Mask, "\033[1;3B", 0, 0},
{ XK_Down, ShiftMask|Mod1Mask,"\033[1;4B", 0, 0},
{ XK_Down, ControlMask, "\033[1;5B", 0, 0},
{ XK_Down, ShiftMask|ControlMask,"\033[1;6B", 0, 0},
{ XK_Down, ControlMask|Mod1Mask,"\033[1;7B", 0, 0},
{ XK_Down,ShiftMask|ControlMask|Mod1Mask,"\033[1;8B",0, 0},
{ XK_Down, XK_ANY_MOD, "\033[B", 0, -1},
{ XK_Down, XK_ANY_MOD, "\033OB", 0, +1},
{ XK_Left, ShiftMask, "\033[1;2D", 0, 0},
{ XK_Left, Mod1Mask, "\033[1;3D", 0, 0},
{ XK_Left, ShiftMask|Mod1Mask,"\033[1;4D", 0, 0},
{ XK_Left, ControlMask, "\033[1;5D", 0, 0},
{ XK_Left, ShiftMask|ControlMask,"\033[1;6D", 0, 0},
{ XK_Left, ControlMask|Mod1Mask,"\033[1;7D", 0, 0},
{ XK_Left,ShiftMask|ControlMask|Mod1Mask,"\033[1;8D",0, 0},
{ XK_Left, XK_ANY_MOD, "\033[D", 0, -1},
{ XK_Left, XK_ANY_MOD, "\033OD", 0, +1},
{ XK_Right, ShiftMask, "\033[1;2C", 0, 0},
{ XK_Right, Mod1Mask, "\033[1;3C", 0, 0},
{ XK_Right, ShiftMask|Mod1Mask,"\033[1;4C", 0, 0},
{ XK_Right, ControlMask, "\033[1;5C", 0, 0},
{ XK_Right, ShiftMask|ControlMask,"\033[1;6C", 0, 0},
{ XK_Right, ControlMask|Mod1Mask,"\033[1;7C", 0, 0},
{ XK_Right,ShiftMask|ControlMask|Mod1Mask,"\033[1;8C",0, 0},
{ XK_Right, XK_ANY_MOD, "\033[C", 0, -1},
{ XK_Right, XK_ANY_MOD, "\033OC", 0, +1},
{ XK_ISO_Left_Tab, ShiftMask, "\033[Z", 0, 0},
{ XK_Return, Mod1Mask, "\033\r", 0, 0},
{ XK_Return, XK_ANY_MOD, "\r", 0, 0},
{ XK_Insert, ShiftMask, "\033[4l", -1, 0},
{ XK_Insert, ShiftMask, "\033[2;2~", +1, 0},
{ XK_Insert, ControlMask, "\033[L", -1, 0},
{ XK_Insert, ControlMask, "\033[2;5~", +1, 0},
{ XK_Insert, XK_ANY_MOD, "\033[4h", -1, 0},
{ XK_Insert, XK_ANY_MOD, "\033[2~", +1, 0},
{ XK_Delete, ControlMask, "\033[M", -1, 0},
{ XK_Delete, ControlMask, "\033[3;5~", +1, 0},
{ XK_Delete, ShiftMask, "\033[2K", -1, 0},
{ XK_Delete, ShiftMask, "\033[3;2~", +1, 0},
{ XK_Delete, XK_ANY_MOD, "\033[3~", -1, 0},
{ XK_Delete, XK_ANY_MOD, "\033[3~", +1, 0},
{ XK_BackSpace, XK_NO_MOD, "\177", 0, 0},
{ XK_BackSpace, Mod1Mask, "\033\177", 0, 0},
{ XK_Home, ShiftMask, "\033[2J", 0, -1},
{ XK_Home, ShiftMask, "\033[1;2H", 0, +1},
{ XK_Home, XK_ANY_MOD, "\033[H", 0, -1},
{ XK_Home, XK_ANY_MOD, "\033[1~", 0, +1},
{ XK_End, ControlMask, "\033[J", -1, 0},
{ XK_End, ControlMask, "\033[1;5F", +1, 0},
{ XK_End, ShiftMask, "\033[K", -1, 0},
{ XK_End, ShiftMask, "\033[1;2F", +1, 0},
{ XK_End, XK_ANY_MOD, "\033[4~", 0, 0},
{ XK_Prior, ControlMask, "\033[5;5~", 0, 0},
{ XK_Prior, ShiftMask, "\033[5;2~", 0, 0},
{ XK_Prior, XK_ANY_MOD, "\033[5~", 0, 0},
{ XK_Next, ControlMask, "\033[6;5~", 0, 0},
{ XK_Next, ShiftMask, "\033[6;2~", 0, 0},
{ XK_Next, XK_ANY_MOD, "\033[6~", 0, 0},
{ XK_F1, XK_NO_MOD, "\033OP" , 0, 0},
{ XK_F1, /* F13 */ ShiftMask, "\033[1;2P", 0, 0},
{ XK_F1, /* F25 */ ControlMask, "\033[1;5P", 0, 0},
{ XK_F1, /* F37 */ Mod4Mask, "\033[1;6P", 0, 0},
{ XK_F1, /* F49 */ Mod1Mask, "\033[1;3P", 0, 0},
{ XK_F1, /* F61 */ Mod3Mask, "\033[1;4P", 0, 0},
{ XK_F2, XK_NO_MOD, "\033OQ" , 0, 0},
{ XK_F2, /* F14 */ ShiftMask, "\033[1;2Q", 0, 0},
{ XK_F2, /* F26 */ ControlMask, "\033[1;5Q", 0, 0},
{ XK_F2, /* F38 */ Mod4Mask, "\033[1;6Q", 0, 0},
{ XK_F2, /* F50 */ Mod1Mask, "\033[1;3Q", 0, 0},
{ XK_F2, /* F62 */ Mod3Mask, "\033[1;4Q", 0, 0},
{ XK_F3, XK_NO_MOD, "\033OR" , 0, 0},
{ XK_F3, /* F15 */ ShiftMask, "\033[1;2R", 0, 0},
{ XK_F3, /* F27 */ ControlMask, "\033[1;5R", 0, 0},
{ XK_F3, /* F39 */ Mod4Mask, "\033[1;6R", 0, 0},
{ XK_F3, /* F51 */ Mod1Mask, "\033[1;3R", 0, 0},
{ XK_F3, /* F63 */ Mod3Mask, "\033[1;4R", 0, 0},
{ XK_F4, XK_NO_MOD, "\033OS" , 0, 0},
{ XK_F4, /* F16 */ ShiftMask, "\033[1;2S", 0, 0},
{ XK_F4, /* F28 */ ControlMask, "\033[1;5S", 0, 0},
{ XK_F4, /* F40 */ Mod4Mask, "\033[1;6S", 0, 0},
{ XK_F4, /* F52 */ Mod1Mask, "\033[1;3S", 0, 0},
{ XK_F5, XK_NO_MOD, "\033[15~", 0, 0},
{ XK_F5, /* F17 */ ShiftMask, "\033[15;2~", 0, 0},
{ XK_F5, /* F29 */ ControlMask, "\033[15;5~", 0, 0},
{ XK_F5, /* F41 */ Mod4Mask, "\033[15;6~", 0, 0},
{ XK_F5, /* F53 */ Mod1Mask, "\033[15;3~", 0, 0},
{ XK_F6, XK_NO_MOD, "\033[17~", 0, 0},
{ XK_F6, /* F18 */ ShiftMask, "\033[17;2~", 0, 0},
{ XK_F6, /* F30 */ ControlMask, "\033[17;5~", 0, 0},
{ XK_F6, /* F42 */ Mod4Mask, "\033[17;6~", 0, 0},
{ XK_F6, /* F54 */ Mod1Mask, "\033[17;3~", 0, 0},
{ XK_F7, XK_NO_MOD, "\033[18~", 0, 0},
{ XK_F7, /* F19 */ ShiftMask, "\033[18;2~", 0, 0},
{ XK_F7, /* F31 */ ControlMask, "\033[18;5~", 0, 0},
{ XK_F7, /* F43 */ Mod4Mask, "\033[18;6~", 0, 0},
{ XK_F7, /* F55 */ Mod1Mask, "\033[18;3~", 0, 0},
{ XK_F8, XK_NO_MOD, "\033[19~", 0, 0},
{ XK_F8, /* F20 */ ShiftMask, "\033[19;2~", 0, 0},
{ XK_F8, /* F32 */ ControlMask, "\033[19;5~", 0, 0},
{ XK_F8, /* F44 */ Mod4Mask, "\033[19;6~", 0, 0},
{ XK_F8, /* F56 */ Mod1Mask, "\033[19;3~", 0, 0},
{ XK_F9, XK_NO_MOD, "\033[20~", 0, 0},
{ XK_F9, /* F21 */ ShiftMask, "\033[20;2~", 0, 0},
{ XK_F9, /* F33 */ ControlMask, "\033[20;5~", 0, 0},
{ XK_F9, /* F45 */ Mod4Mask, "\033[20;6~", 0, 0},
{ XK_F9, /* F57 */ Mod1Mask, "\033[20;3~", 0, 0},
{ XK_F10, XK_NO_MOD, "\033[21~", 0, 0},
{ XK_F10, /* F22 */ ShiftMask, "\033[21;2~", 0, 0},
{ XK_F10, /* F34 */ ControlMask, "\033[21;5~", 0, 0},
{ XK_F10, /* F46 */ Mod4Mask, "\033[21;6~", 0, 0},
{ XK_F10, /* F58 */ Mod1Mask, "\033[21;3~", 0, 0},
{ XK_F11, XK_NO_MOD, "\033[23~", 0, 0},
{ XK_F11, /* F23 */ ShiftMask, "\033[23;2~", 0, 0},
{ XK_F11, /* F35 */ ControlMask, "\033[23;5~", 0, 0},
{ XK_F11, /* F47 */ Mod4Mask, "\033[23;6~", 0, 0},
{ XK_F11, /* F59 */ Mod1Mask, "\033[23;3~", 0, 0},
{ XK_F12, XK_NO_MOD, "\033[24~", 0, 0},
{ XK_F12, /* F24 */ ShiftMask, "\033[24;2~", 0, 0},
{ XK_F12, /* F36 */ ControlMask, "\033[24;5~", 0, 0},
{ XK_F12, /* F48 */ Mod4Mask, "\033[24;6~", 0, 0},
{ XK_F12, /* F60 */ Mod1Mask, "\033[24;3~", 0, 0},
{ XK_F13, XK_NO_MOD, "\033[1;2P", 0, 0},
{ XK_F14, XK_NO_MOD, "\033[1;2Q", 0, 0},
{ XK_F15, XK_NO_MOD, "\033[1;2R", 0, 0},
{ XK_F16, XK_NO_MOD, "\033[1;2S", 0, 0},
{ XK_F17, XK_NO_MOD, "\033[15;2~", 0, 0},
{ XK_F18, XK_NO_MOD, "\033[17;2~", 0, 0},
{ XK_F19, XK_NO_MOD, "\033[18;2~", 0, 0},
{ XK_F20, XK_NO_MOD, "\033[19;2~", 0, 0},
{ XK_F21, XK_NO_MOD, "\033[20;2~", 0, 0},
{ XK_F22, XK_NO_MOD, "\033[21;2~", 0, 0},
{ XK_F23, XK_NO_MOD, "\033[23;2~", 0, 0},
{ XK_F24, XK_NO_MOD, "\033[24;2~", 0, 0},
{ XK_F25, XK_NO_MOD, "\033[1;5P", 0, 0},
{ XK_F26, XK_NO_MOD, "\033[1;5Q", 0, 0},
{ XK_F27, XK_NO_MOD, "\033[1;5R", 0, 0},
{ XK_F28, XK_NO_MOD, "\033[1;5S", 0, 0},
{ XK_F29, XK_NO_MOD, "\033[15;5~", 0, 0},
{ XK_F30, XK_NO_MOD, "\033[17;5~", 0, 0},
{ XK_F31, XK_NO_MOD, "\033[18;5~", 0, 0},
{ XK_F32, XK_NO_MOD, "\033[19;5~", 0, 0},
{ XK_F33, XK_NO_MOD, "\033[20;5~", 0, 0},
{ XK_F34, XK_NO_MOD, "\033[21;5~", 0, 0},
{ XK_F35, XK_NO_MOD, "\033[23;5~", 0, 0},
};
/*
* Selection types' masks.
* Use the same masks as usual.
* Button1Mask is always unset, to make masks match between ButtonPress.
* ButtonRelease and MotionNotify.
* If no match is found, regular selection is used.
*/
static uint selmasks[] = {
[SEL_RECTANGULAR] = Mod1Mask,
};
/*
* Printable characters in ASCII, used to estimate the advance width
* of single wide characters.
*/
static char ascii_printable[] =
" !\"#$%&'()*+,-./0123456789:;<=>?"
"@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_"
"`abcdefghijklmnopqrstuvwxyz{|}~";

18
st/patches.list Normal file
View file

@ -0,0 +1,18 @@
# st-flexipatch patches the rice turns on, one per line, without "_PATCH".
# st/build.sh flips each to 1 in patches.h. See patches.def.h upstream for the full list.
ALPHA # transparency (alt + a / alt + s)
BOXDRAW # gapless box-drawing and braille characters
CSI_22_23 # save/restore window title (nvim)
KEYBOARDSELECT # select text with the keyboard (ctrl + shift + escape)
LIGATURES # ligatures via harfbuzz
NEWTERM # new terminal in the same directory (ctrl + shift + return)
OPENURLONCLICK # ctrl + click opens a link
SCROLLBACK # shift/alt + page up/down, alt + j/k/u/d
SCROLLBACK_MOUSE # shift + wheel scrolls back
SCROLLBACK_MOUSE_ALTSCREEN # plain wheel scrolls back outside full-screen apps
SIXEL # images in the terminal (chafa, yazi, fzf previews)
UNDERCURL # curly underlines (nvim diagnostics)
WIDE_GLYPHS # Nerd Font icons drawn full width
XRESOURCES # font and colours from Xresources (rice-theme)
XRESOURCES_RELOAD # re-read them on USR1, so theme changes reach open terminals

34
templates/Xresources.tpl Normal file
View file

@ -0,0 +1,34 @@
! Rendered by rice-theme from colors.toml - edit templates/Xresources.tpl instead.
! st reads these through its xresources patch; reopen st (or send it USR1) to pick them up.
*.foreground: {{ fg }}
*.background: {{ bg }}
*.cursorColor: {{ accent }}
*.color0: {{ bg }}
*.color1: {{ red }}
*.color2: {{ green }}
*.color3: {{ yellow }}
*.color4: {{ blue }}
*.color5: {{ magenta }}
*.color6: {{ cyan }}
*.color7: {{ fg_alt }}
*.color8: {{ muted }}
*.color9: {{ bright_red }}
*.color10: {{ bright_green }}
*.color11: {{ bright_yellow }}
*.color12: {{ bright_blue }}
*.color13: {{ bright_magenta }}
*.color14: {{ bright_cyan }}
*.color15: {{ fg }}
st.font: {{ font }}:pixelsize=14:antialias=true:autohint=true
st.alpha: {{ alpha }}
st.borderpx: 15
Xcursor.theme: {{ cursor_theme }}
Xcursor.size: {{ cursor_size }}
Xft.antialias: 1
Xft.hinting: 1
Xft.hintstyle: hintslight
Xft.rgba: rgb

View file

@ -0,0 +1,8 @@
#!/bin/sh
# Rendered by rice-theme from colors.toml - edit templates/bspwm-colors.sh.tpl instead.
bspc config focused_border_color "{{ accent }}"
bspc config active_border_color "{{ border }}"
bspc config normal_border_color "{{ surface }}"
bspc config presel_feedback_color "{{ magenta }}"
bspc config border_width {{ window_border }}
bspc config window_gap {{ gap }}

29
templates/dunst.conf.tpl Normal file
View file

@ -0,0 +1,29 @@
# Rendered by rice-theme from colors.toml - edit templates/dunst.conf.tpl instead.
# Loaded as a drop-in (~/.config/dunst/dunstrc.d/50-theme.conf), so it only
# carries colours; layout and behaviour live in dunstrc.
[global]
font = "{{ font }} 10"
icon_theme = "{{ icon_theme }}, Adwaita, hicolor"
[urgency_low]
background = "{{ bg }}{{ alpha_hex }}"
foreground = "{{ fg_alt }}"
frame_color = "{{ border }}"
highlight = "{{ dim }}"
[urgency_normal]
background = "{{ bg }}{{ alpha_hex }}"
foreground = "{{ fg }}"
frame_color = "{{ border }}"
highlight = "{{ accent }}"
[urgency_critical]
background = "{{ bg }}{{ alpha_hex }}"
foreground = "{{ fg }}"
frame_color = "{{ urgent }}"
highlight = "{{ urgent }}"
[osd]
frame_color = "{{ accent }}"
highlight = "{{ accent }}"

View file

@ -0,0 +1,30 @@
// Rendered by rice-theme from colors.toml - edit templates/eww-colors.scss.tpl instead.
$bg: {{ bg }};
$bg-alt: {{ bg_alt }};
$surface: {{ surface }};
$overlay: {{ overlay }};
$selection: {{ selection }};
$border: {{ border }};
$fg: {{ fg }};
$fg-alt: {{ fg_alt }};
$dim: {{ dim }};
$muted: {{ muted }};
$accent: {{ accent }};
$accent-alt: {{ accent_alt }};
$urgent: {{ urgent }};
$red: {{ red }};
$green: {{ green }};
$yellow: {{ yellow }};
$blue: {{ blue }};
$magenta: {{ magenta }};
$cyan: {{ cyan }};
$font: "{{ font }}";
$radius: {{ radius }}px;
$radius-small: {{ radius_small }}px;
$border-width: {{ border_width }}px;
$gap: {{ gap }}px;
$alpha: {{ alpha }};

6
templates/fzf.sh.tpl Normal file
View file

@ -0,0 +1,6 @@
# Rendered by rice-theme from colors.toml - edit templates/fzf.sh.tpl instead.
# Sourced by ~/.bashrc so fzf, ff and Ctrl+R pickers match rofi.
export FZF_DEFAULT_OPTS="\
--color=bg:-1,bg+:{{ selection }},fg:{{ fg_alt }},fg+:{{ fg }},hl:{{ accent }},hl+:{{ accent }} \
--color=border:{{ border }},header:{{ accent }},info:{{ dim }},marker:{{ green }},pointer:{{ urgent }},prompt:{{ urgent }},spinner:{{ cyan }} \
--prompt=' ' --pointer='▌' --marker='+' --border=rounded --layout=reverse --height=60% --info=inline-right"

65
templates/gtk.css.tpl Normal file
View file

@ -0,0 +1,65 @@
/* Rendered by rice-theme from colors.toml - edit templates/gtk.css.tpl instead.
Linked as ~/.config/gtk-3.0/gtk.css and ~/.config/gtk-4.0/gtk.css. */
/* GTK3 / Adwaita named colours */
@define-color theme_bg_color {{ bg_alt }};
@define-color theme_fg_color {{ fg }};
@define-color theme_base_color {{ bg }};
@define-color theme_text_color {{ fg }};
@define-color theme_selected_bg_color {{ accent }};
@define-color theme_selected_fg_color {{ bg }};
@define-color theme_unfocused_bg_color {{ bg_alt }};
@define-color theme_unfocused_fg_color {{ fg }};
@define-color theme_unfocused_base_color {{ bg }};
@define-color theme_unfocused_text_color {{ fg }};
@define-color theme_unfocused_selected_bg_color {{ selection }};
@define-color theme_unfocused_selected_fg_color {{ fg }};
@define-color insensitive_bg_color {{ bg_alt }};
@define-color insensitive_fg_color {{ dim }};
@define-color insensitive_base_color {{ bg }};
@define-color borders {{ border }};
@define-color unfocused_borders {{ border }};
@define-color wm_bg {{ surface }};
@define-color wm_border {{ border }};
@define-color wm_title {{ fg }};
@define-color wm_unfocused_title {{ dim }};
@define-color success_color {{ green }};
@define-color warning_color {{ yellow }};
@define-color error_color {{ red }};
@define-color link_color {{ accent }};
@define-color visited_link_color {{ magenta }};
@define-color content_view_bg {{ bg }};
/* libadwaita / GTK4 named colours */
@define-color accent_color {{ accent }};
@define-color accent_bg_color {{ accent }};
@define-color accent_fg_color {{ bg }};
@define-color destructive_color {{ red }};
@define-color window_bg_color {{ bg_alt }};
@define-color window_fg_color {{ fg }};
@define-color view_bg_color {{ bg }};
@define-color view_fg_color {{ fg }};
@define-color headerbar_bg_color {{ surface }};
@define-color headerbar_fg_color {{ fg }};
@define-color headerbar_border_color {{ border }};
@define-color headerbar_backdrop_color {{ bg_alt }};
@define-color headerbar_shade_color rgba(0, 0, 0, 0.36);
@define-color sidebar_bg_color {{ surface }};
@define-color sidebar_fg_color {{ fg }};
@define-color sidebar_backdrop_color {{ bg_alt }};
@define-color sidebar_border_color {{ border }};
@define-color sidebar_shade_color rgba(0, 0, 0, 0.36);
@define-color secondary_sidebar_bg_color {{ bg_alt }};
@define-color secondary_sidebar_fg_color {{ fg }};
@define-color card_bg_color {{ surface }};
@define-color card_fg_color {{ fg }};
@define-color card_shade_color rgba(0, 0, 0, 0.36);
@define-color dialog_bg_color {{ surface }};
@define-color dialog_fg_color {{ fg }};
@define-color popover_bg_color {{ overlay }};
@define-color popover_fg_color {{ fg }};
@define-color popover_shade_color rgba(0, 0, 0, 0.36);
@define-color thumbnail_bg_color {{ surface }};
@define-color thumbnail_fg_color {{ fg }};
@define-color shade_color rgba(0, 0, 0, 0.36);
@define-color scrollbar_outline_color {{ border }};

21
templates/imv-config.tpl Normal file
View file

@ -0,0 +1,21 @@
# Rendered by rice-theme from colors.toml - edit templates/imv-config.tpl instead.
# Linked as ~/.config/imv/config.
[options]
background = {{ bg_strip }}
overlay_font = {{ font }}:11
overlay_text_color = {{ fg_strip }}
overlay_background_color = {{ bg_strip }}
overlay_background_alpha = cc
[binds]
# Copy the picture to the clipboard
<Ctrl+c> = exec sh -c 'xclip -selection clipboard -t "$(file -b --mime-type "$1")" -i "$1"' _ "$imv_current_file"
# Make it the wallpaper
<Ctrl+w> = exec rice-wallpaper set "$imv_current_file"
# Rotate the file on disk by 90 degrees
<Ctrl+r> = exec mogrify -rotate 90 "$imv_current_file"
# Print it
<Ctrl+p> = exec lp "$imv_current_file"
# Move to the trash and quit / move to the trash and show the next one
<Ctrl+x> = exec gio trash -- "$imv_current_file"; quit
<Ctrl+Shift+X> = exec gio trash -- "$imv_current_file"; close

30
templates/kitty.conf.tpl Normal file
View file

@ -0,0 +1,30 @@
# Rendered by rice-theme from colors.toml - edit templates/kitty.conf.tpl instead.
font_family {{ font }}
bold_font auto
italic_font auto
bold_italic_font auto
background_opacity {{ alpha }}
background {{ bg }}
foreground {{ fg }}
cursor {{ accent }}
cursor_text_color {{ bg }}
selection_background {{ selection }}
selection_foreground {{ fg }}
url_color {{ accent }}
color0 {{ bg }}
color1 {{ red }}
color2 {{ green }}
color3 {{ yellow }}
color4 {{ blue }}
color5 {{ magenta }}
color6 {{ cyan }}
color7 {{ fg_alt }}
color8 {{ muted }}
color9 {{ bright_red }}
color10 {{ bright_green }}
color11 {{ bright_yellow }}
color12 {{ bright_blue }}
color13 {{ bright_magenta }}
color14 {{ bright_cyan }}
color15 {{ fg }}

View file

@ -0,0 +1,21 @@
; Rendered by rice-theme from colors.toml - edit templates/polybar-colors.ini.tpl instead.
[colors]
bg = {{ bg }}
bg-trans = #{{ alpha_hex }}{{ bg_strip }}
surface = {{ surface }}
border = #99{{ border_strip }}
text = {{ fg }}
dim = {{ dim }}
muted = {{ muted }}
accent = {{ accent }}
urgent = {{ urgent }}
red = {{ red }}
green = {{ green }}
yellow = {{ yellow }}
blue = {{ blue }}
magenta = {{ magenta }}
cyan = {{ cyan }}
[fonts]
main = {{ font }}:style=Bold:pixelsize=10;2
large = {{ font }}:style=Bold:pixelsize=12;2

View file

@ -0,0 +1,35 @@
; Rendered by rice-theme from colors.toml - edit templates/polybar-modules.ini.tpl instead.
; Polybar only resolves ${colors.x} when it is the whole value, so modules that
; colour part of their own text live here with the theme's colours written in.
[module/date]
type = internal/date
interval = 1
date = %a %d
time = %H:%M
label = "%{A1:rice-panel calendar:}%{A3:rice-reminder new:}%{F{{ accent }}}%{F-} %time% %{F{{ dim }}}|%{F-} %date%%{A}%{A}"
format-padding = 1
[module/cpu]
type = custom/script
exec = rice-sysinfo --cpu --normal-color '{{ accent }}' --critical-color '{{ urgent }}'
click-left = rice-panel power
format = "%{F{{ accent }}}CPU%{F-} <label>"
format-padding = 1
interval = 2
[module/ram]
type = custom/script
exec = rice-sysinfo --ram --normal-color '{{ yellow }}' --critical-color '{{ urgent }}'
click-left = rice-panel power
format = "%{F{{ yellow }}}RAM%{F-} <label>"
format-padding = 1
interval = 2
[module/gpu]
type = custom/script
exec = rice-sysinfo --gpu --normal-color '{{ magenta }}' --critical-color '{{ urgent }}'
click-left = rice-panel power
format = "%{F{{ magenta }}}GPU%{F-} <label>"
format-padding = 1
interval = 2

View file

@ -0,0 +1,18 @@
/* Rendered by rice-theme from colors.toml - edit templates/rofi-colors.rasi.tpl instead. */
* {
bg: {{ bg }};
bg-trans: {{ bg }}{{ alpha_hex }};
surface: {{ surface }};
fg: {{ fg }};
dim: {{ dim }};
accent: {{ accent }};
urgent: {{ urgent }};
good: {{ green }};
warn: {{ yellow }};
border-col: {{ border }};
select-bg: {{ accent }}2b;
select-fg: {{ fg }};
font-main: "{{ font }} 11";
font-bold: "{{ font }} Bold 12";
font-entry: "{{ font }} 12";
}

View file

@ -0,0 +1,76 @@
# Rendered by rice-theme from colors.toml - edit templates/starship.toml.tpl instead.
# One line, like bash's user@host prompt with colour and git:
# sarthak@arch ~/rice main !?
"$schema" = "https://starship.rs/config-schema.json"
add_newline = false
command_timeout = 300
palette = "rice"
format = "$username$hostname$directory$git_branch$git_state$git_status$jobs$cmd_duration$character"
[palettes.rice]
fg = "{{ fg }}"
dim = "{{ dim }}"
accent = "{{ accent }}"
urgent = "{{ urgent }}"
green = "{{ green }}"
yellow = "{{ yellow }}"
magenta = "{{ magenta }}"
[username]
show_always = true
format = "[$user]($style)"
style_user = "bold accent"
style_root = "bold urgent"
[hostname]
ssh_only = false
trim_at = "."
format = "[@](dim)[$hostname]($style) "
style = "bold magenta"
[directory]
style = "bold fg"
truncation_length = 3
truncate_to_repo = false
truncation_symbol = "…/"
read_only = " 󰌾"
read_only_style = "urgent"
format = "[$path]($style)[$read_only]($read_only_style) "
[git_branch]
symbol = " "
style = "dim"
format = "[$symbol$branch]($style) "
[git_state]
style = "yellow"
format = "[$state( $progress_current/$progress_total)]($style) "
[git_status]
style = "yellow"
format = "([$all_status$ahead_behind]($style) )"
conflicted = "="
untracked = "?"
stashed = ""
modified = "!"
staged = "+"
renamed = "»"
deleted = "✘"
ahead = "⇡"
behind = "⇣"
diverged = "⇕"
[jobs]
symbol = "✦"
style = "dim"
format = "[$symbol$number]($style) "
[cmd_duration]
min_time = 2000
format = "[$duration](dim) "
[character]
success_symbol = "[](bold accent)"
error_symbol = "[](bold urgent)"
vimcmd_symbol = "[](bold green)"

View file

@ -0,0 +1,15 @@
# Rendered by rice-theme from colors.toml - edit templates/xsecurelock.env.tpl instead.
# Sourced by rice-lock before it starts xsecurelock.
export XSECURELOCK_FONT="{{ font }}:size=13"
export XSECURELOCK_BACKGROUND_COLOR="{{ bg }}"
export XSECURELOCK_AUTH_BACKGROUND_COLOR="{{ bg }}"
export XSECURELOCK_AUTH_FOREGROUND_COLOR="{{ fg }}"
export XSECURELOCK_AUTH_WARNING_COLOR="{{ urgent }}"
export XSECURELOCK_PASSWORD_PROMPT="asterisks"
export XSECURELOCK_SHOW_DATETIME=1
export XSECURELOCK_DATETIME_FORMAT="%A %d %B %H:%M"
export XSECURELOCK_SHOW_HOSTNAME=0
export XSECURELOCK_SHOW_USERNAME=0
export XSECURELOCK_SAVER=saver_xscreensaver
export XSECURELOCK_BLANK_TIMEOUT=600
export XSECURELOCK_BLANK_DPMS_STATE=off

Some files were not shown because too many files have changed in this diff Show more