diff --git a/home/oli/.config/i3blocks/config b/home/oli/.config/i3blocks/config
new file mode 100644
index 0000000..8d7083e
--- /dev/null
+++ b/home/oli/.config/i3blocks/config
@@ -0,0 +1,70 @@
+# Global stuff
+separator=true
+separator_block_width=8
+
+[fortune]
+markup=pango
+command=~/bin/i3blocks/fortune
+interval=300
+
+[time]
+markup=pango
+color=#b1e5c8
+background=#2E8B57
+command=echo " $(date +'%A, %d %B %R %Z') "
+interval=10
+
+[wifi]
+markup=pango
+command=~/bin/i3blocks/wifi
+interval=20
+
+[cputemp]
+markup=pango
+command=~/bin/i3blocks/cputemp
+interval=20
+
+# CPU load
+[cpu]
+markup=pango
+command=~/bin/i3blocks/cpu
+interval=3
+
+[mem]
+markup=pango
+command=~/bin/i3blocks/mem
+interval=30
+
+[audio]
+markup=pango
+command=~/bin/i3blocks/audio
+interval=once
+signal=1
+
+[mic_mute]
+markup=pango
+command=~/bin/i3blocks/mic_mute
+interval=once
+signal=2
+
+# Indoor temperature and humidity
+[homeassistant-slow]
+markup=pango
+command=~/bin/i3blocks/homeassistant-slow
+interval=60
+
+# Power draw
+[homeassistant-fast]
+markup=pango
+command=~/bin/i3blocks/homeassistant-fast
+interval=10
+
+[archupdate]
+markup=pango
+command=~/bin/i3blocks/archupdates
+interval=28800 # 4h
+
+[weather]
+markup=pango
+command=~/bin/i3blocks/weather
+interval=900
diff --git a/home/oli/.config/sway/config b/home/oli/.config/sway/config
index ee47b0b..1efbf31 100644
--- a/home/oli/.config/sway/config
+++ b/home/oli/.config/sway/config
@@ -44,7 +44,7 @@ output * bg ~/data/local/wallpaper/anders-jilden-AkUR27wtaxs-unsplash.jpg fill
# You can get the names of your outputs by running: swaymsg -t get_outputs
# BSP-style window tiling
-exec_always autotiling
+exec_always autotiling-rs
# This will lock your screen after 300 seconds of inactivity, then turn off
# your displays after another 300 seconds, and turn your screens back on when
@@ -197,11 +197,12 @@ bindsym ctrl+alt+shift+r mode "RESIZE"
# Volume control with PipeWire
# https://github.com/xkbcommon/libxkbcommon/blob/master/include/xkbcommon/xkbcommon-keysyms.h
# Get the device ID with `wpctl status` or use @DEFAULT_AUDIO_SINK@
-bindsym --locked XF86AudioMute exec wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle
-# bindsym --release XF86AudioMute exec pkill -USR1 swaybar
+# The signal is mapped to a i3block configuration
+bindsym --locked XF86AudioMute exec wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle && pkill -SIGRTMIN+1 i3blocks
# `-l` sets max volume level
-bindsym --locked XF86AudioLowerVolume exec wpctl set-volume @DEFAULT_AUDIO_SINK@ 0.02- -l 1.0
-bindsym --locked XF86AudioRaiseVolume exec wpctl set-volume @DEFAULT_AUDIO_SINK@ 0.02+ -l 1.0
+bindsym --locked XF86AudioLowerVolume exec wpctl set-volume @DEFAULT_AUDIO_SINK@ 0.02- -l 1.0 && pkill -SIGRTMIN+1 i3blocks
+bindsym --locked XF86AudioRaiseVolume exec wpctl set-volume @DEFAULT_AUDIO_SINK@ 0.02+ -l 1.0 && pkill -SIGRTMIN+1 i3blocks
+
# FIXME: https://wiki.archlinux.org/title/MPRIS
# bindsym --locked XF86AudioPause exec wpctl set-mute 48 toggle
@@ -212,8 +213,10 @@ bindsym Mod4+Shift+Space exec grimshot --notify save window
# man 5 sway-bar
bar {
position bottom
- font "BerkeleyMono Nerd Font Mono Normal Bold 11"
- status_command ~/.config/sway/status.sh
+ # font "BerkeleyMono Nerd Font Mono Normal Bold 10"
+ font "BerkeleyMono Nerd Font Mono Normal Regular 11"
+ # status_command ~/.config/sway/status.sh
+ status_command i3blocks
strip_workspace_name no
strip_workspace_numbers no
colors {
diff --git a/home/oli/bin/i3blocks/_utils b/home/oli/bin/i3blocks/_utils
new file mode 100644
index 0000000..7607b7a
--- /dev/null
+++ b/home/oli/bin/i3blocks/_utils
@@ -0,0 +1,33 @@
+#!/usr/bin/env bash
+#
+# Shared utilities for i3blocks scripts.
+# Source this file to get color variables and the icon() helper.
+# Colors are available immediately on source — no function call needed.
+
+# Generic
+ok_fg="#f0f0f0"
+warn1_bg="#facc15"
+warn1_fg="#36454f"
+warn2_bg="#cc8d22"
+warn2_fg="#36454f"
+crit_bg="#710f3d"
+crit_fg="#c0c8c6"
+
+# Audio
+mute_bg="#303030"
+mute_fg="#e0e0e0"
+
+# Weather
+frost_bg="#5b8dd9"
+frost_fg="#f0f0f0"
+cold_bg="#7ec8e3"
+cold_fg="#1a1a2e"
+warm_bg="#facc15"
+warm_fg="#36454f"
+hot_bg="#cc3300"
+hot_fg="#f0f0f0"
+
+# Wrap a glyph in Pango markup for consistent icon sizing and vertical alignment
+icon() {
+ echo "$1"
+}
diff --git a/home/oli/bin/i3blocks/archupdates b/home/oli/bin/i3blocks/archupdates
new file mode 100755
index 0000000..0da4bbb
--- /dev/null
+++ b/home/oli/bin/i3blocks/archupdates
@@ -0,0 +1,22 @@
+#!/usr/bin/env bash
+
+. ~/bin/i3blocks/_utils
+
+# Check if updated Arch packages can be installed
+pkgupdate() {
+ local pkgcount=$(checkupdates | wc -l)
+ if [[ $pkgcount -ge 1 && $pkgcount -lt 20 ]]; then
+ local bg=$mute_bg
+ local fg=$mute_fg
+ elif [[ $pkgcount -ge 20 ]]; then
+ local bg=$warn1_bg
+ local fg=$warn1_fg
+ fi
+ local stat="$pkgcount pkg updates"
+
+ if [[ -n "$bg" ]]; then
+ echo -e "$stat\n"
+ fi
+}
+
+pkgupdate
diff --git a/home/oli/bin/i3blocks/audio b/home/oli/bin/i3blocks/audio
new file mode 100755
index 0000000..e37390a
--- /dev/null
+++ b/home/oli/bin/i3blocks/audio
@@ -0,0 +1,53 @@
+#!/usr/bin/env bash
+#
+# Display audio volume/mute status
+# Depends on signals in sway config for XF86AudioMute, XF86AudioLowerVolume and
+# XF86AudioRaiseVolume
+
+. ~/bin/i3blocks/_utils
+
+# Get default audio sink volume and mute status
+audio_volume() {
+ local wpctl_output
+ wpctl_output=$(wpctl get-volume @DEFAULT_AUDIO_SINK@)
+ local volume
+ volume=$(echo "$wpctl_output" | awk '{print $NF * 100}')
+ local muted
+ muted=$(echo "$wpctl_output" | grep -c '\[MUTED\]')
+
+ local bg fg icon stat
+
+ if [[ "$muted" -ge 1 ]]; then
+ bg=$mute_bg
+ fg=$mute_fg
+ stat="$(printf '%.0f' "$volume")%"
+ elif [[ $(echo "$volume >= 80" | bc -l) == "1" ]]; then
+ bg=$crit_bg
+ fg=$crit_fg
+ stat="$(printf '%.0f' "$volume")%"
+ elif [[ $(echo "$volume >= 65" | bc -l) == "1" ]]; then
+ bg=$warn2_bg
+ fg=$warn2_fg
+ stat="$(printf '%.0f' "$volume")%"
+ elif [[ $(echo "$volume >= 50" | bc -l) == "1" ]]; then
+ bg=$warn1_bg
+ fg=$warn1_fg
+ stat="$(printf '%.0f' "$volume")%"
+ else
+ stat="$(printf '%.0f' "$volume")%"
+ fi
+
+ if [[ -n "$bg" ]]; then
+ echo -e "SND $stat \n"
+ else
+ echo -e "SND $stat \n"
+ fi
+}
+
+case "$BLOCK_BUTTON" in
+ 1) wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle ;;
+ 4) wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+ --limit 1.0 ;;
+ 5) wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%- ;;
+esac
+
+audio_volume
diff --git a/home/oli/bin/i3blocks/cpu b/home/oli/bin/i3blocks/cpu
new file mode 100755
index 0000000..d26a69f
--- /dev/null
+++ b/home/oli/bin/i3blocks/cpu
@@ -0,0 +1,70 @@
+#!/usr/bin/env bash
+#
+# Displays per-core CPU usage as Unicode block characters.
+# Uses a temp file to track deltas between intervals.
+#
+# ▄ = load below 50%
+# █ = load at or above 50%
+# Colors escalate through warn1 -> warn2 -> crit thresholds.
+
+PREV_FILE="/tmp/i3blocks_cpu_prev"
+
+. ~/bin/i3blocks/_utils
+# Read current idle and total ticks for each core from /proc/stat
+declare -a cur_idle cur_total
+i=0
+while IFS=' ' read -r cpu user nice system idle iowait irq softirq rest; do
+ [[ "$cpu" =~ ^cpu[0-9]+$ ]] || continue
+ cur_idle[$i]=$((idle + iowait))
+ cur_total[$i]=$((user + nice + system + idle + iowait + irq + softirq))
+ ((i++))
+done < /proc/stat
+num_cores=$i
+
+# Load previous snapshot if available
+declare -a prev_idle prev_total
+if [[ -f "$PREV_FILE" ]]; then
+ i=0
+ while IFS=' ' read -r p_idle p_total; do
+ prev_idle[$i]=$p_idle
+ prev_total[$i]=$p_total
+ ((i++))
+ done < "$PREV_FILE"
+fi
+
+# Save current snapshot for next run
+for ((i=0; i "$PREV_FILE"
+
+# Build output — one block character per core
+output=""
+for ((i=0; i${char}"
+ else
+ output+="${char}"
+ fi
+done
+
+echo -e "CPU $output\n"
diff --git a/home/oli/bin/i3blocks/cputemp b/home/oli/bin/i3blocks/cputemp
new file mode 100755
index 0000000..b0f2f09
--- /dev/null
+++ b/home/oli/bin/i3blocks/cputemp
@@ -0,0 +1,27 @@
+#!/usr/bin/env bash
+
+. ~/bin/i3blocks/_utils
+
+# Get CPU temperature
+temp_cpu() {
+ local cpu=$(expr $(cat /sys/class/thermal/thermal_zone1/temp) / 1000)
+ if [[ $(echo "$cpu >= 60" | bc -l ) == "1" ]] && [[ $(echo "$cpu < 70" | bc -l ) == "1" ]]; then
+ local bg=$warn1_bg
+ local fg=$warn1_fg
+ elif [[ $(echo "$cpu >= 70" | bc -l ) == "1" ]] && [[ $(echo "$cpu < 80" | bc -l ) == "1" ]]; then
+ local bg=$warn2_bg
+ local fg=$warn2_fg
+ elif [[ $(echo "$cpu >= 80" | bc -l ) == "1" ]]; then
+ local bg=$crit_bg
+ local fg=$crit_fg
+ fi
+ local stat="CPU $cpu°C"
+
+ if [[ -n "$bg" ]]; then
+ echo -e "$stat\n"
+ else
+ echo -e "$stat\n"
+ fi
+}
+
+temp_cpu
diff --git a/home/oli/bin/i3blocks/fortune b/home/oli/bin/i3blocks/fortune
new file mode 100755
index 0000000..3a889d5
--- /dev/null
+++ b/home/oli/bin/i3blocks/fortune
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+echo -e "$(fortune -s -n 60 | sed 's/^[[:space:]]*//' | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g' | sed 's/[[:space:]]*$//')\n"
diff --git a/home/oli/bin/i3blocks/homeassistant-fast b/home/oli/bin/i3blocks/homeassistant-fast
new file mode 100755
index 0000000..25de801
--- /dev/null
+++ b/home/oli/bin/i3blocks/homeassistant-fast
@@ -0,0 +1,18 @@
+#!/usr/bin/env bash
+#
+# Needs a .env.homeassistant with an API token in ~/.config/sway/.env.homeassistant
+# TODO:
+# - HA optimize API calls: https://community.home-assistant.io/t/get-limited-number-of-states-entities-when-using-api-states/830323
+
+homeassistant_url="http://10.7.1.11:8123/api/states"
+. ~/.config/sway/.env.homeassistant
+
+. ~/bin/i3blocks/_utils
+
+ha() {
+ local power=$(curl -s -H "Authorization: Bearer $ha_token" -H "Content-Type: application/json" $homeassistant_url/sensor.inspelning_oli_power | jq -r '.state')
+ local stat="PWR ${power}W"
+ echo "$stat"
+}
+
+ha
diff --git a/home/oli/bin/i3blocks/homeassistant-slow b/home/oli/bin/i3blocks/homeassistant-slow
new file mode 100755
index 0000000..e3a2ca0
--- /dev/null
+++ b/home/oli/bin/i3blocks/homeassistant-slow
@@ -0,0 +1,19 @@
+#!/usr/bin/env bash
+#
+# Needs a .env.homeassistant with an API token in ~/.config/sway/.env.homeassistant
+# TODO:
+# - HA optimize API calls: https://community.home-assistant.io/t/get-limited-number-of-states-entities-when-using-api-states/830323
+
+homeassistant_url="http://10.7.1.11:8123/api/states"
+. ~/.config/sway/.env.homeassistant
+
+. ~/bin/i3blocks/_utils
+
+ha() {
+ local temp_indoor=$(curl -s -H "Authorization: Bearer $ha_token" -H "Content-Type: application/json" $homeassistant_url/sensor.vindstyrka_oli_temperature | jq -r '.state')
+ local humidity_indoor=$(curl -s -H "Authorization: Bearer $ha_token" -H "Content-Type: application/json" $homeassistant_url/sensor.vindstyrka_oli_humidity | jq -r '.state')
+ local stat="IN ${temp_indoor}°C ${humidity_indoor}%"
+ echo $stat
+}
+
+ha
diff --git a/home/oli/bin/i3blocks/mem b/home/oli/bin/i3blocks/mem
new file mode 100755
index 0000000..f40a11b
--- /dev/null
+++ b/home/oli/bin/i3blocks/mem
@@ -0,0 +1,28 @@
+#!/usr/bin/env bash
+
+. ~/bin/i3blocks/_utils
+
+# Get total memory usage and show highest consumer
+mem(){
+ local memused=$(free | grep Mem | awk '{printf "%.0f\n", $3/$2 * 100.0}')
+ local memproc=$(basename "$(ps --no-headers -A --sort -rss -o cmd | head -1 | awk {'print $1'})")
+ if [[ $(echo "$memused >= 50" | bc -l ) == "1" ]] && [[ $(echo "$memused < 60" | bc -l ) == "1" ]]; then
+ local bg=$warn1_bg
+ local fg=$warn1_fg
+ elif [[ $(echo "$memused >= 60" | bc -l ) == "1" ]] && [[ $(echo "$memused < 70" | bc -l ) == "1" ]]; then
+ local bg=$warn2_bg
+ local fg=$warn2_fg
+ elif [[ $(echo "$memused >= 70" | bc -l ) == "1" ]]; then
+ local bg=$crit_bg
+ local fg=$crit_fg
+ fi
+ local stat="MEM ${memused}% [$memproc]"
+
+ if [[ -n "$bg" ]]; then
+ echo -e " $stat \n"
+ else
+ echo -e "$stat\n"
+ fi
+}
+
+mem
diff --git a/home/oli/bin/i3blocks/mic_mute b/home/oli/bin/i3blocks/mic_mute
new file mode 100755
index 0000000..56f86b2
--- /dev/null
+++ b/home/oli/bin/i3blocks/mic_mute
@@ -0,0 +1,33 @@
+#!/usr/bin/env bash
+#
+# Shows microphone mute status for the default audio source.
+# Left click toggles mute.
+
+. ~/bin/i3blocks/_utils
+
+mic_mute() {
+ if [[ "$BLOCK_BUTTON" == "1" ]]; then
+ wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle
+ fi
+
+ local wpctl_output
+ wpctl_output=$(wpctl get-volume @DEFAULT_AUDIO_SOURCE@)
+ local muted
+ muted=$(echo "$wpctl_output" | grep -c '\[MUTED\]')
+
+ local bg fg icon stat
+
+ if [[ "$muted" -ge 1 ]]; then
+ bg=$mute_bg
+ fg=$mute_fg
+ stat="MUT"
+ else
+ bg=$crit_bg
+ fg=$crit_fg
+ stat="REC"
+ fi
+
+ echo -e "MIC $stat \n"
+}
+
+mic_mute
diff --git a/home/oli/bin/i3blocks/weather b/home/oli/bin/i3blocks/weather
new file mode 100755
index 0000000..bee2a4a
--- /dev/null
+++ b/home/oli/bin/i3blocks/weather
@@ -0,0 +1,99 @@
+#!/usr/bin/env bash
+#
+# Fetches current weather for Herzogenbuchsee, Switzerland via open-meteo.com.
+# No API key required.
+#
+# WMO weather codes: https://open-meteo.com/en/docs#weathervariables
+
+# Coordinates for Herzogenbuchsee, CH
+LAT="47.1897"
+LON="7.7058"
+# NOTE: MeteoSwiss ICON-CH1 model (~1 km resolution) via open-meteo, matching
+# the MeteoSwiss mobile app data source.
+API_URL="https://api.open-meteo.com/v1/forecast"
+MODEL="meteoswiss_icon_ch1"
+
+. ~/bin/i3blocks/_utils
+
+# Map WMO weather interpretation codes to icons (Nerd Font nf-weather-*)
+wmo_icon() {
+ local code=$1
+ case $code in
+ 0) echo "clear" ;; # Clear sky
+ 1) echo "mostly clear" ;; # Mainly clear
+ 2) echo "partly cloudy" ;; # Partly cloudy
+ 3) echo "overcast" ;; # Overcast
+ 45|48) echo "fog" ;; # Fog / depositing rime fog
+ 51|53|55) echo "drizzle" ;; # Drizzle: light, moderate, dense
+ 56|57) echo "freezing drizzle" ;; # Freezing drizzle: light, dense
+ 61|63|65) echo "rain" ;; # Rain: slight, moderate, heavy
+ 66|67) echo "freezing rain" ;; # Freezing rain: light, heavy
+ 71|73|75|77) echo "snow" ;; # Snow: slight, moderate, heavy, snow grains
+ 80|81|82) echo "showers" ;; # Rain showers: slight, moderate, violent
+ 85|86) echo "snow showers" ;; # Snow showers: slight, heavy
+ 95) echo "thunderstorm" ;; # Thunderstorm
+ 96|99) echo "thunderstorm+hail" ;; # Thunderstorm with hail: slight, heavy
+ *) echo "?" ;;
+ esac
+}
+
+weather() {
+ local response
+ response=$(curl -sf "${API_URL}?latitude=${LAT}&longitude=${LON}¤t=temperature_2m,apparent_temperature,weather_code,wind_speed_10m&daily=sunrise,sunset&wind_speed_unit=kmh&timezone=Europe%2FZurich&models=${MODEL}")
+
+ if [[ $? -ne 0 || -z "$response" ]]; then
+ echo " N/A"
+ exit 0
+ fi
+
+ local temp apparent code wind sunrise sunset
+ temp=$(echo "$response" | jq -r '.current.temperature_2m')
+ apparent=$(echo "$response" | jq -r '.current.apparent_temperature')
+ code=$(echo "$response" | jq -r '.current.weather_code')
+ wind=$(echo "$response" | jq -r '.current.wind_speed_10m')
+ sunrise=$(echo "$response" | jq -r '.daily.sunrise[0]')
+ sunset=$(echo "$response" | jq -r '.daily.sunset[0]')
+
+ # Compare current time against sunrise/sunset (format: YYYY-MM-DDTHH:MM)
+ local now sunrise_epoch sun_label sun_time
+ now=$(date +%s)
+ sunrise_epoch=$(date -d "$sunrise" +%s)
+
+ if [[ $now -lt $sunrise_epoch ]]; then
+ sun_label=""
+ sun_time=$(date -d "$sunrise" +%H:%M)
+ else
+ sun_label=""
+ sun_time=$(date -d "$sunset" +%H:%M)
+ fi
+
+ local label
+ label=$(wmo_icon "$code")
+
+ local bg fg stat
+
+ if [[ $(echo "$temp < 0" | bc -l) == "1" ]]; then
+ bg=$frost_bg
+ fg=$frost_fg
+ elif [[ $(echo "$temp < 8" | bc -l) == "1" ]]; then
+ bg=$cold_bg
+ fg=$cold_fg
+ elif [[ $(echo "$temp >= 28" | bc -l) == "1" ]]; then
+ bg=$hot_bg
+ fg=$hot_fg
+ elif [[ $(echo "$temp >= 22" | bc -l) == "1" ]]; then
+ bg=$warm_bg
+ fg=$warm_fg
+ fi
+
+ # NOTE: apparent temperature shown in parentheses as "feels like"
+ stat="$label $(printf '%.0f' "$temp")°C ($(printf '%.0f' "$apparent")°C) $(printf '%.0f' "$wind") km/h $sun_label $sun_time"
+
+ if [[ -n "$bg" ]]; then
+ echo -e "OUT $stat "
+ else
+ echo -e "OUT $stat "
+ fi
+}
+
+weather
diff --git a/home/oli/bin/i3blocks/wifi b/home/oli/bin/i3blocks/wifi
new file mode 100755
index 0000000..3044716
--- /dev/null
+++ b/home/oli/bin/i3blocks/wifi
@@ -0,0 +1,28 @@
+#!/usr/bin/env bash
+
+. ~/bin/i3blocks/_utils
+
+# Get WiFi signal strength
+wifi() {
+ local wifidbm_orig=$(iwctl station wlan0 show | awk '/[[:space:]]RSSI/{print $2}')
+ local wifidbm=$((wifidbm_orig * -1))
+ if [[ $(echo "$wifidbm >= 50" | bc -l ) == "1" ]] && [[ $(echo "$wifidbm < 60" | bc -l ) == "1" ]]; then
+ local bg=$warn1_bg
+ local fg=$warn1_fg
+ elif [[ $(echo "$wifidbm >= 60" | bc -l ) == "1" ]] && [[ $(echo "$wifidbm < 70" | bc -l ) == "1" ]]; then
+ local bg=$warn2_bg
+ local fg=$warn2_fg
+ elif [[ $(echo "$wifidbm >= 70" | bc -l ) == "1" ]]; then
+ local bg=$crit_bg
+ local fg=$crit_fg
+ fi
+ local stat="NET -${wifidbm} dBm"
+
+ if [[ -n "$bg" ]]; then
+ echo -e "$stat\n"
+ else
+ echo -e "$stat\n"
+ fi
+}
+
+wifi