Skip to content

Instantly share code, notes, and snippets.

@an-prata
Created September 19, 2023 22:31
Show Gist options
  • Save an-prata/4395349af6913717876a16eca26176f2 to your computer and use it in GitHub Desktop.
Save an-prata/4395349af6913717876a16eca26176f2 to your computer and use it in GitHub Desktop.
Updated Wiki page for #2517 of Alexays/Waybar

The custom module displays either the output of a script or static text. To display static text, specify only the format field.

Config

Addressed by custom/<name>

option typeof default description
exec string
exec-if string The path to a script, which determines if the script in exec should be executed.
exec will be executed if the exit code of exec-if equals 0.
exec-on-event bool true If an event command is set (e.g. on-click or on-scroll-up) then re-execute the script after executing the event command.
return-type string See return-type
interval integer The interval (in seconds) in which the information gets polled.
Use once if you want to execute the module only on startup. You can update it manually with a signal. If no interval or signal is defined, it is assumed that the out script loops it self. If a signal is defined then the script will run once on startup and will will only update with a signal.
restart-interval integer The restart interval (in seconds).
Can't be used with the interval option, so only with continuous scripts.
Once the script exit, it'll be re-executed after the restart-interval.
signal integer The signal number used to update the module. The number is valid between 1 and N, where SIGRTMIN+N = SIGRTMAX. If no interval is defined then a signal will be the only way to update the module.
format string {} The format, how information should be displayed. On {} data gets inserted.
format-icons array/object/string If the type is an array, then based on the set percentage, the corresponding icon gets selected (low to high).
If the type is an object, then the icon will be selected according to alt string from the output.
If the type is a string, it will be pasted as is.
NOTE: Arrays can be nested into objects. Icons will be selected first according to alt then percentage.
rotate integer Positive value to rotate the text label.
max-length integer The maximum length in character the module should display.
on-click string Command to execute when clicked on the module.
on-click-middle string Command to execute when you middle clicked on the module using mousewheel.
on-click-right string Command to execute when you right clicked on the module.
on-scroll-up string Command to execute when scrolling up on the module.
on-scroll-down string Command to execute when scrolling down on the module.
smooth-scrolling-threshold double Threshold to be used when scrolling.
tooltip bool true Option to enable tooltip on hover.
escape bool false Option to enable escaping of script output

Return-Type:

  • When return-type is set to json, Waybar expects the exec-script to output its data in JSON format. This should look like this: {"text": "$text", "alt": "$alt", "tooltip": "$tooltip", "class": "$class", "percentage": $percentage }. This means the output should also be on a single line. This can be achieved by piping the output of your script through jq --unbuffered --compact-output. The class parameter also accepts an array of strings.
  • If nothing or an invalid option is specified Waybar expects i3blocks style output, where values are newline separated. This should look like this: $text\n$tooltip\n$class

class is a CSS class, to apply different styles in style.css

Format replacements:

string replacement
{} Output of the script.
{percentage} Percentage which can be set via a json return-type.
{icon} An icon from 'format-icons' according to percentage.

Style

  • #custom-<name>
  • #custom-<name>.<class>
    • <class> can be set by the script. For more information see return-type

Examples:

dunst:

~/.config/waybar/config

"custom/dunst": {
    "exec": "~/.config/waybar/scripts/dunst.sh",
    "on-click": "dunstctl set-paused toggle",
    "restart-interval": 1,
}

~/.config/waybar/scripts/dunst.sh

#!/bin/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:

#!/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
Generic MediaPlayer:

Supports vlc, mpv, RhythmBox, web browsers, cmus, mpd, spotify and others.

"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:
"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:
"custom/mpd": {
    "format": "♪ {}",
    //"max-length": 15,
    "interval": 10, 
    "exec": "mpc current", 
    "exec-if": "pgrep mpd",
    "on-click": "mpc toggle",
    "on-click-right": "sonata"
}   
cmus:
"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
"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

"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

#!/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
"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.

DeaDBeeF
"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)

"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
"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 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.
#!/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.

"custom/weather": {
    "exec": "$XDG_CONFIG_HOME/waybar/get_weather.sh Berlin+Germany",
    "return-type": "json",
    "format": "{}",
    "tooltip": true,
    "interval": 3600
}
#!/usr/bin/env bash
# get_weather.sh
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

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.

"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.

"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
"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
"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"
}
See also
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment