One of the things I love most about my current setup is that I know exactly what every process is doing and why it’s running. When I started customizing my swaybar, I wanted the same principle to apply — no status bar plugins I don’t understand, no external dependencies I can’t explain. Just shell commands I wrote myself.
This post walks through how I built a custom swaybar status line with battery percentage, charging status, volume level, and mute detection.
Dependencies
Before anything else, you’ll need PipeWire running for audio support. If you’re on a minimal install like I am, these packages won’t be there by default:
sudo apt install pipewire pipewire-pulse wireplumber alsa-utils pulseaudio-utils A quick breakdown of what each one does:
- pipewire - The core audio server
- pipewire-pulse - A compatibility layer so apps built for PulseAudio work with PipeWire
- wireplumber - The session manager that routes audio to the right hardware
- alsa-utils - Low level tools for interacting with your audio hardware directly
- pulseaudio-utils - Gives you
pactl, the command line tool for controlling volume
Since I manage my own process lifecycle through Sway rather than systemd, I also mask the systemd units so there’s no double-launching:
systemctl --user mask pipewire pipewire-pulse wireplumber
systemctl --user mask pipewire.socket pipewire-pulse.socket And start everything from my Sway config in the correct order:
exec pipewire
exec sh -c "sleep 1 && pipewire-pulse"
exec sh -c "sleep 2 && wireplumber" The sh -c wrapper is necessary because Sway’s exec doesn’t invoke a shell — it runs binaries directly, so shell operators like && won’t work without it.
Building the Status Command
The swaybar status_command runs a loop that echoes a string every second. That string becomes what you see in the bar. Here’s the full script:
status_command while true;
do
battery_percent=$(cat /sys/class/power_supply/BAT0/capacity);
battery_status=$(cat /sys/class/power_supply/BAT0/status);
date=$(date +'%Y-%m-%d %X');
volume_raw=$(pactl get-sink-volume '@DEFAULT_SINK@');
regex_ex="d+(?=%)";
volume_level=$(echo $volume_raw | grep -oP $regex_ex | head -1);
mute_raw=$(pactl get-sink-mute '@DEFAULT_SINK@');
volume_display=$([ "$mute_raw" = "Mute: yes" ] && echo "MUTED" || echo "Vol $volume_level%");
message="$volume_display | $battery_status $battery_percent% | $date";
echo $message;
sleep 1;
done Breaking It Down
Battery is straightforward — the kernel exposes battery information as plain text files under /sys/class/power_supply/. Reading them is just cat.
battery_percent=$(cat /sys/class/power_supply/BAT0/capacity)
battery_status=$(cat /sys/class/power_supply/BAT0/status) capacity gives you a number like 82. status gives you Charging, Discharging, or Full.
Volume requires a bit more work. pactl get-sink-volume returns a verbose string with decibels, balance, and both channels. We only want the percentage:
volume_raw=$(pactl get-sink-volume '@DEFAULT_SINK@')
volume_level=$(echo $volume_raw | grep -oP 'd+(?=%)' | head -1) The @DEFAULT_SINK@ needs single quotes — without them the shell may try to interpret the @ symbols. The grep pattern uses a lookahead ((?=%)) to match digits immediately followed by a % sign without including the % in the result. head -1 takes only the first match since both audio channels return the same value.
Mute detection uses a ternary-style one liner rather than a full if/then/fi block, which doesn’t play nicely inside an inline status command:
mute_raw=$(pactl get-sink-mute '@DEFAULT_SINK@')
volume_display=$([ "$mute_raw" = "Mute: yes" ] && echo "MUTED" || echo "Vol $volume_level%") If muted, display MUTED. Otherwise display Vol 72% or whatever the current level is.
Volume Keybindings
To control volume from the keyboard, add these to your Sway config using the wpctl tool which respects a 100% ceiling by default:
bindsym XF86AudioRaiseVolume exec wpctl set-volume -l 1.0 @DEFAULT_AUDIO_SINK@ 5%+
bindsym XF86AudioLowerVolume exec wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-
bindsym XF86AudioMute exec wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle The XF86 key names are Linux’s standardized names for multimedia keys — your physical volume buttons on the keyboard map to these automatically.
The Result
A status bar that shows exactly what I need, built entirely from commands I understand:
Vol 72% | Discharging 81% | 2025-03-06 23:58:01 Or when muted:
MUTED | Charging 94% | 2025-03-06 23:58:01 No plugins. No dependencies beyond what’s needed for audio to work anyway. Every line of that script I can explain from memory.
That’s the goal.
Still up at 2am tweaking configs. No regrets. 🚀