Files
dotfiles/dot_config/waybar/waybar.wiki/Module:-Custom:-Examples.md
s0wlz (Matthias Puchstein) 8018b53353 feat: finalize migration to chezmoi and modernize configuration logic
- Modularize Hyprland config into hyprland.d/
- Implement infinitely scalable monitor/workspace logic using templates and loop-based data structures
- Consolidate host-specific configs (hyprlock, hyprpaper, waybar) into single templates
- Resolve waybar symlink conflict and fix template execution errors
- Integrate chezmoi data variables for scale, resolution, and peripherals
2025-12-27 22:52:43 +01:00

587 lines
18 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This page contains brief examples, with code provided here directly, of custom modules:
#### CPU History:
Shows a CPU graph like this:
<img width="227" height="17" alt="2025-07-23-172546_hyprshot" src="https://github.com/user-attachments/assets/e6b6cd88-5f83-45d7-b559-a285155bd291" />
Full usage example in my dotfiles: https://github.com/cargodog/dot-config/tree/master/waybar
`~/.config/waybar/config`
```jsonc
"custom/cpuhistory": {
"exec": "~/.config/waybar/scripts/cpu_history.py -d 60",
"format": "<span color='#FFA500'>  </span>{}",
"interval": 1,
"return-type": "json",
"on-click": "~/.config/waybar/scripts/cpu_history.py toggle"
},
```
* Requires python 3 and the python psutil package
* Set `-d` to set the number of bars in the graph
* For example, setting `"interval": 1` and `-d 60` means the graph will show the last 60 seconds of CPU usage
* `on-click` action toggles between graph view and single measurement
`~/.config/waybar/config/scripts/cpu_history.py`
```python
#!/usr/bin/env python3
"""CPU Usage Graph Monitor - Displays a unicode graph of CPU usage history using Braille characters."""
import json
import os
import sys
import psutil
import argparse
from pathlib import Path
# Configuration
CACHE_DIR = Path(os.environ.get('XDG_CACHE_HOME', Path.home() / '.cache'))
HISTORY_FILE = CACHE_DIR / 'cpu_usage_history.json'
DEFAULT_HISTORY_DEPTH = 40
# Braille patterns for vertical bar graphs
BRAILLE_PATTERNS = {
(0, 0): '', (1, 0): '', (2, 0): '', (3, 0): '', (4, 0): '',
(0, 1): '', (1, 1): '', (2, 1): '', (3, 1): '', (4, 1): '',
(0, 2): '', (1, 2): '', (2, 2): '', (3, 2): '', (4, 2): '',
(0, 3): '', (1, 3): '', (2, 3): '', (3, 3): '', (4, 3): '',
(0, 4): '', (1, 4): '', (2, 4): '', (3, 4): '', (4, 4): '',
}
def get_braille_char(left_val, right_val):
"""Convert two percentage values (0-100) to a single Braille character."""
# Convert percentages to levels (1-4), minimum 1 to always show at least one dot
left_level = max(1, min(int(left_val * 4 / 100), 4))
right_level = max(1, min(int(right_val * 4 / 100), 4))
return BRAILLE_PATTERNS.get((left_level, right_level), '')
def load_data():
"""Load data from cache file."""
try:
with open(HISTORY_FILE, 'r') as f:
data = json.load(f)
# Handle legacy format
if isinstance(data, list):
return {"history": data, "show_graph": True}
return data
except (FileNotFoundError, json.JSONDecodeError):
return {"history": [], "show_graph": True}
def save_data(data):
"""Save data to cache file."""
CACHE_DIR.mkdir(parents=True, exist_ok=True)
with open(HISTORY_FILE, 'w') as f:
json.dump(data, f)
def main():
parser = argparse.ArgumentParser(description='CPU Usage Graph Monitor with Braille display')
parser.add_argument('command', nargs='?', help='Command: toggle')
parser.add_argument('-d', '--depth', type=int, default=DEFAULT_HISTORY_DEPTH,
help=f'History depth (default: {DEFAULT_HISTORY_DEPTH})')
args = parser.parse_args()
# Ensure even depth for Braille pairs
history_depth = args.depth + (args.depth % 2)
data = load_data()
# Handle toggle command
if args.command == "toggle":
data["show_graph"] = not data.get("show_graph", True)
save_data(data)
return
# Get CPU usage
per_core = psutil.cpu_percent(interval=0.1, percpu=True)
current_usage = sum(per_core) / len(per_core)
# Update history
history = data["history"]
history.append(current_usage)
# Keep only needed history
if len(history) > history_depth:
history = history[-history_depth:]
# Generate output
if data.get("show_graph", True):
# Pad with zeros if needed
padded = [0.0] * (history_depth - len(history)) + history
# Build graph
graph = ''.join(
get_braille_char(
padded[i],
padded[i + 1] if i + 1 < history_depth else 0.0
)
for i in range(0, history_depth, 2)
)
text = f"[{graph}]"
else:
text = f"{current_usage:.1f}%"
# Save and output
data["history"] = history
save_data(data)
# Build color-coded tooltip
tooltip_lines = []
for i, usage in enumerate(per_core):
if usage >= 80:
color = "#ff6b6b" # Red for high load
elif usage >= 60:
color = "#feca57" # Yellow for medium-high load
elif usage >= 40:
color = "#48dbfb" # Cyan for medium load
else:
color = "#1dd1a1" # Green for low load
tooltip_lines.append(f'<span color="{color}">Core {i}: {usage:5.1f}%</span>')
# Output for waybar
print(json.dumps({
"text": text,
"tooltip": '\n'.join(tooltip_lines),
"class": "cpu-history"
}))
if __name__ == "__main__":
main()
```
#### dunst:
`~/.config/waybar/config`
```jsonc
"custom/dunst": {
"exec": "~/.config/waybar/scripts/dunst.sh",
"on-click": "dunstctl set-paused toggle",
"restart-interval": 1,
}
```
`~/.config/waybar/scripts/dunst.sh`
```bash
#!/usr/bin/env bash
COUNT=$(dunstctl count waiting)
ENABLED=
DISABLED=
if [ $COUNT != 0 ]; then DISABLED="$COUNT"; fi
if dunstctl is-paused | grep -q "false" ; then echo $ENABLED; else echo $DISABLED; fi
```
Or if you want a version that reacts to dbus events instead:
```bash
#!/usr/bin/env bash
set -euo pipefail
readonly ENABLED=' '
readonly DISABLED=' '
dbus-monitor path='/org/freedesktop/Notifications',interface='org.freedesktop.DBus.Properties',member='PropertiesChanged' --profile |
while read -r _; do
PAUSED="$(dunstctl is-paused)"
if [ "$PAUSED" == 'false' ]; then
CLASS="enabled"
TEXT="$ENABLED"
else
CLASS="disabled"
TEXT="$DISABLED"
COUNT="$(dunstctl count waiting)"
if [ "$COUNT" != '0' ]; then
TEXT="$DISABLED ($COUNT)"
fi
fi
printf '{"text": "%s", "class": "%s"}\n' "$TEXT" "$CLASS"
done
```
#### NVIDIA GPU (with nvidia-smi)
```jsonc
"custom/nvidia": {
"exec": "nvidia-smi --query-gpu=utilization.gpu,temperature.gpu --format=csv,nounits,noheader | sed 's/\\([0-9]\\+\\), \\([0-9]\\+\\)/\\1% 🌡️\\2°C/g'",
"format": "{} 🖥️",
"interval": 2
}
```
#### Generic MediaPlayer:
Supports vlc, mpv, RhythmBox, web browsers, cmus, mpd, spotify and others.
```jsonc
"custom/media": {
"format": "{icon} {}",
"escape": true,
"return-type": "json",
"max-length": 40,
"on-click": "playerctl play-pause",
"on-click-right": "playerctl stop",
"smooth-scrolling-threshold": 10, // This value was tested using a trackpad, it should be lowered if using a mouse.
"on-scroll-up": "playerctl next",
"on-scroll-down": "playerctl previous",
"exec": "$HOME/.config/waybar/mediaplayer.py 2> /dev/null", // Script in resources/custom_modules folder
}
```
#### Spotify:
```jsonc
"custom/spotify": {
"format": "{icon} {}",
"escape": true,
"return-type": "json",
"max-length": 40,
"interval": 30, // Remove this if your script is endless and write in loop
"on-click": "playerctl -p spotify play-pause",
"on-click-right": "killall spotify",
"smooth-scrolling-threshold": 10, // This value was tested using a trackpad, it should be lowered if using a mouse.
"on-scroll-up" : "playerctl -p spotify next",
"on-scroll-down" : "playerctl -p spotify previous",
"exec": "$HOME/.config/waybar/mediaplayer.py 2> /dev/null", // Script in resources/custom_modules folder
"exec-if": "pgrep spotify"
}
```
#### mpd:
```jsonc
"custom/mpd": {
"format": "♪ {}",
//"max-length": 15,
"interval": 10,
"exec": "mpc current",
"exec-if": "pgrep mpd",
"on-click": "mpc toggle",
"on-click-right": "sonata"
}
```
#### cmus:
```jsonc
"custom/cmus": {
"format": "♪ {}",
//"max-length": 15,
"interval": 10,
"exec": "cmus-remote -C \"format_print '%a - %t'\"", // artist - title
"exec-if": "pgrep cmus",
"on-click": "cmus-remote -u", //toggle pause
"escape": true //handle markup entities
}
```
#### MPRIS controller
```jsonc
"custom/media": {
"format": "{icon}{}",
"return-type": "json",
"format-icons": {
"Playing": " ",
"Paused": " ",
},
"max-length":70,
"exec": "playerctl -a metadata --format '{\"text\": \"{{playerName}}: {{artist}} - {{markup_escape(title)}}\", \"tooltip\": \"{{playerName}} : {{markup_escape(title)}}\", \"alt\": \"{{status}}\", \"class\": \"{{status}}\"}' -F",
"on-click": "playerctl play-pause",
}
```
#### Pipewire:
Uses Wireplumber
`~/.config/waybar/config`
```jsonc
"custom/pipewire": {
"tooltip": false,
"max-length": 6,
"exec": "$HOME/.config/waybar/scripts/pipewire.sh",
"on-click": "pavucontrol",
"on-click-right": "qpwgraph"
}
```
`~/.config/waybar/scripts/pipewire.sh`
```bash
#!/bin/bash
set -e
# https://blog.dhampir.no/content/sleeping-without-a-subprocess-in-bash-and-how-to-sleep-forever
snore() {
local IFS
[[ -n "${_snore_fd:-}" ]] || exec {_snore_fd}<> <(:)
read -r ${1:+-t "$1"} -u $_snore_fd || :
}
DELAY=0.2
while snore $DELAY; do
WP_OUTPUT=$(wpctl get-volume @DEFAULT_AUDIO_SINK@)
if [[ $WP_OUTPUT =~ ^Volume:[[:blank:]]([0-9]+)\.([0-9]{2})([[:blank:]].MUTED.)?$ ]]; then
if [[ -n ${BASH_REMATCH[3]} ]]; then
printf "MUTE\n"
else
VOLUME=$((10#${BASH_REMATCH[1]}${BASH_REMATCH[2]}))
ICON=(
""
""
""
)
if [[ $VOLUME -gt 50 ]]; then
printf "%s" "${ICON[0]} "
elif [[ $VOLUME -gt 25 ]]; then
printf "%s" "${ICON[1]} "
elif [[ $VOLUME -ge 0 ]]; then
printf "%s" "${ICON[2]} "
fi
printf "$VOLUME%%\n"
fi
fi
done
exit 0
```
#### Pacman
```jsonc
"custom/pacman": {
"format": "{} ",
"interval": "once",
"exec": "pacman_packages",
"on-click": "update-system",
"signal": 8
}
//alternate
"custom/pacman": {
"format": "{} ",
"interval": 3600, // every hour
"exec": "checkupdates | wc -l", // # of updates
"exec-if": "exit 0", // always run; consider advanced run conditions
"on-click": "termite -e 'sudo pacman -Syu'; pkill -SIGRTMIN+8 waybar", // update system
"signal": 8
}
```
You can use the signal and update the number of available packages with `pkill -RTMIN+8 waybar`.
#### XBPS
Show available updates for void linux.
`~/.config/waybar/config`
```jsonc
"custom/xbps": {
"format": "{} ",
"return-type": "json",
"tooltip": true,
"interval": "3600",
"exec": "~/.config/waybar/custom/xbps-updates.sh"
},
```
`~/.config/waybar/custom/xbps-updates.sh`
```bash
#!/bin/bash
pkgs=$(xbps-install -nuM | awk '{print $1}')
pkg_count=$(echo $pkgs | wc -w)
pkg_list=$(echo $pkgs | sed 's/ /\\r/g')
echo "{\"text\":\"$pkg_count\", \"tooltip\":\"$pkg_list\"}"
```
#### DeaDBeeF
```jsonc
"custom/deadbeef": {
"format": " {}",
"max-length": 50,
"interval": 10,
"exec": "deadbeef --nowplaying-tf '{\"text\": \"%title%\", \"tooltip\":\"%artist% - %title%\",\"class\":\"$if(%isplaying%,playing,not-playing)\"}'",
"return-type": "json",
"exec-if": "pgrep deadbeef",
"on-click": "deadbeef --toggle-pause"
}
```
#### VPN indicator
(the indicator is quite silly and only checks whether a tunnel exists or not)
```jsonc
"custom/vpn": {
"format": "VPN ",
"exec": "echo '{\"class\": \"connected\"}'",
"exec-if": "test -d /proc/sys/net/ipv4/conf/tun0",
"return-type": "json",
"interval": 5
}
```
#### Github notifications
```jsonc
"custom/github": {
"format": "{} ",
"return-type": "json",
"interval": 60,
"exec": "$HOME/.config/waybar/github.sh",
"on-click": "xdg-open https://github.com/notifications"
}
```
1. Make sure [`jq`](https://stedolan.github.io/jq/) is installed.
2. Create `notifications.token`, a personal access token, with `notifications` in scope at https://github.com/settings/tokens.
3. Create `github.sh` with the contents below, replacing `username` with your own.
```bash
#!/bin/bash
token=`cat ${HOME}/.config/github/notifications.token`
count=`curl -u username:${token} https://api.github.com/notifications | jq '. | length'`
if [[ "$count" != "0" ]]; then
echo '{"text":'$count',"tooltip":"$tooltip","class":"$class"}'
fi
```
#### Weather
Replace `Berlin+Germany` with your own city.
`~/.config/waybar/config`
```jsonc
"custom/weather": {
"exec": "${HOME}/.config/waybar/scripts/get_weather.sh Berlin+Germany",
"return-type": "json",
"format": "{}",
"tooltip": true,
"interval": 3600
}
```
`~/.config/waybar/scripts/get_weather.sh`
```bash
#!/usr/bin/env bash
for i in {1..5}
do
text=$(curl -s "https://wttr.in/$1?format=1")
if [[ $? == 0 ]]
then
text=$(echo "$text" | sed -E "s/\s+/ /g")
tooltip=$(curl -s "https://wttr.in/$1?format=4")
if [[ $? == 0 ]]
then
tooltip=$(echo "$tooltip" | sed -E "s/\s+/ /g")
echo "{\"text\":\"$text\", \"tooltip\":\"$tooltip\"}"
exit
fi
fi
sleep 2
done
echo "{\"text\":\"error\", \"tooltip\":\"error\"}"
```
#### Sway Scratchpad Indicator:
Requires [`jq`](https://stedolan.github.io/jq/)
Get all the scratchpad nodes. Shows the count as module text and the window class/app_id, id, and name on hover, and doesn't display anything if there are no nodes in the scratchpad.
```jsonc
"custom/scratchpad-indicator": {
"interval": 3,
"return-type": "json",
"exec": "swaymsg -t get_tree | jq --unbuffered --compact-output '(recurse(.nodes[]) | select(.name == \"__i3_scratch\") | .focus) as $scratch_ids | [.. | (.nodes? + .floating_nodes?) // empty | .[] | select(.id |IN($scratch_ids[]))] as $scratch_nodes | if ($scratch_nodes|length) > 0 then { text: \"\\($scratch_nodes | length)\", tooltip: $scratch_nodes | map(\"\\(.app_id // .window_properties.class) (\\(.id)): \\(.name)\") | join(\"\\n\") } else empty end'",
"format": "{} 🗗",
"on-click": "exec swaymsg 'scratchpad show'",
"on-click-right": "exec swaymsg 'move scratchpad'"
}
```
A simpler version, that only shows the number of windows when there is at least one (hidden when there are 0). Shows no additional info on hover.
```jsonc
"custom/scratchpad_indicator": {
"interval": 3,
"exec": "swaymsg -t get_tree | jq 'recurse(.nodes[]) | first(select(.name==\"__i3_scratch\")) | .floating_nodes | length | select(. >= 1)'",
"format": "{} ",
"on-click": "swaymsg 'scratchpad show'",
"on-click-right": "swaymsg 'move scratchpad'"
}
```
#### Sway output scaling toggle
```jsonc
"custom/output-scale": {
"format": "{icon} {}",
"return-type": "json",
"format-icons": { // These are FontAwesome 4 icons. Update them as needed.
"scale": " \uf0b2",
"noscale": "\uf066"
},
"exec-on-event": true,
"interval": "once",
"exec": "( swaymsg -r -t get_outputs | jq '.[0].scale' | xargs test 1 == ) && echo '{\"alt\": \"noscale\"}' || echo '{\"alt\":\"scale\"}'",
"exec-if": "sleep 0.1", // Give enough time for `sway output` command changes to propagate so we can read them in the next `exec`
"on-click": "( swaymsg -r -t get_outputs | jq '.[0].scale' | xargs test 1 = ) && swaymsg output DP-1 scale 1.4 || swaymsg output DP-1 scale 1"
}
```
1. Change the desired scaling parameter in `on-click` configuration.
2. Update the correct output from `DP-1` to the one you have.
3. Change the index `[0]` in `exec` and `on-click` if you have more than one output, and need to adjust non-zero output.
#### Display current Pulseaudio sink and cycle between sinks on click
```jsonc
"custom/pulseaudio-cycle": {
"return-type": "json",
"exec-on-event": true,
"interval": "5s",
"exec" "pactl --format=json list sinks | jq -cM --unbuffered \"map(select(.name == \\\"$(pactl get-default-sink)\\\"))[0].properties | [.\\\"media.name\\\",.\\\"alsa.name\\\",.\\\"node.nick\\\",.\\\"alsa.long_card_name\\\"] | map(select(length>0))[0] | {text:.}\"",
"exec-if": "sleep 0.1", // Give enough time for `pactl get-default-sink` to update
"on-click": "pactl --format=json list sinks short | jq -cM --unbuffered \"[.[].name] | .[((index(\\\"$(pactl get-default-sink)\\\")+1)%length)]\" | xargs pactl set-default-sink"
}
```
#### Calendar with CalDAV integration
Requires [plann](https://github.com/tobixen/plann)
```
#!/usr/bin/env bash
PLANN=$HOME/.pyenv/versions/plann/bin/plann
printf '{"text":"'
printf "󰸘 $(date +'%m-%d (%a)') "
printf "󰅐 $(date +'%H:%M')"
printf '",'
printf '"tooltip":"%s"' "$($PLANN --caldav-url CALDAV_URL --caldav-username CALDAV_USER --caldav-password CALDAV_PASSWORD --calendar-name 'CALDAV_CALENDAR_NAME' agenda | head --lines -1 | sed 's/$/\\n/' | tr -d '\n' | head --bytes -2)"
printf '}'
```
Remove `--calendar-name` option to displays the last events across all calendars.
#### Measure power draw (of PC for example) on Tuya Smart power plug over Zigbee2MQTT
Requires [mosquitto](https://github.com/eclipse/mosquitto) and [jq](https://github.com/jqlang/jq)
```
"custom/tuya": {
"format": "{}w",
"exec": "mosquitto_sub -h YOUR_HOST -t 'zigbee2mqtt/YOUR_SMART_DEV' | jq '.power' --unbuffered",
"exec-if": "exit 0",
"restart-interval": 60,
"escape": true,
}
```
#### Simple VRR/Adaptive sync toggle for sway
```
"custom/adaptive-sync" : {
"format": " VRR{} ",
"exec-on-event": true,
"interval": "once",
"exec": "swaymsg -r -t get_outputs | jq '.[0].adaptive_sync_status'",
"on-click": "swaymsg output DP-2 adaptive_sync on",
"on-click-right": "swaymsg output DP-2 adaptive_sync off"
}
```