Automatic Visual Feedback for System Volume Change in I3wm via Dunst

tomoviktor

Tömő Viktor

Posted on June 27, 2024

Automatic Visual Feedback for System Volume Change in I3wm via Dunst

Simple yet powerful all in one stytem volume watcher and changer script for linux. Let me show you my small script.

Introduction

I switched to the i3 tiling based window manager. Because it's a whole different environment and thinking, it was very different from what I was used to. The volume buttons were working on my keyboard, but I didn't get any visual feedback. Furthermore, the volume percentage could go down below zero and increase up to more than hundread percent. There were times when I was confused why the keys stopped working, but the actual hidden reason was that the volume's value was somehow -500 percent, so increasing it by 5 percent via my keys would have taken a little time.

To solve all this, I decided to write my own zsh script. If you are familiar with linux scripting you may ask: why didn't I use bash? It's simple, I did lots of bash scripting in school already so I decided to try out zsh (not like I discovered big differences).

The script is available at my GitHub .dotfiles repository named change-volume. In this blog I will explain how to use it and how does the code work.

Using the script

There are two use cases for the script: watch, change the current volume.

Watch listens to volume changes and automatically shows notifications. The watcher actually watches meaning it also works if the volume isn't changed via this script.

Chaning the volume is just a wrapper that takes care of minimizing and maximizing the values so they don't go under or over a certain limit. You must decrease or increase by percentage and you also have the option to mute too. When the source is muted and a value change is requested the script first unmutes the source and the next volume change will actually do something with the source's value.

Starting a watch:

volume-changer "watch" 
Enter fullscreen mode Exit fullscreen mode

Increase or decrease the volume or mute it fully (which automatically toggles):

volume-changer "+5%"
volume-changer "+50%"
volume-changer "-5%"
volume-changer "-21%"

volume-changer "full"
Enter fullscreen mode Exit fullscreen mode

My script also logs details via logger so I can inspect it if it doesn't seem to work.

If you think don't want to bother with the code, you can just download it from GitHub. I made it so that in the top few lines you can easily configure few basic things. Don't forget to download the icons too if you need them.

The code

Sending notifications

The base of all of this is notifications. Because my i3 came with dunst and I liked the simple look of it I decided to use it as the notification daemon. I wanted to have 3 simple things: display current status of the volume via text, display an icon so it is somewhat prettier, display the volume level via a progress bar. Lucily all these are possible via dunst.

How do you actually send notifications? Just use dunstify. I also found a nice website that uses examples to show how it works.

Important note is that the progress bar feature is available since dunst version 1.6.0, so make sure you have a updated version. For me, the apt install on my Ubuntu downloaded a very outdated version of dunst which din't supported progress bars, so I decided to build it from source.

I created a perfect function for showing alerts:

lastalerttext=""
show_alert() {
  if [[ $lastalerttext != $3 ]]; then
    lastalerttext="$3"
    dunstify --replace=1111 --timeout=1500 --icon="$1" --hints=int:value:"$2%" "change-volume" "$3"
  fi
}
# usage: show_alert [iconpath] [progressbar percentage] [text]
Enter fullscreen mode Exit fullscreen mode

Chaning the volume

I wanted to make it so you must provide two type of values for changing the percentage: +[NUMBER]% or -[NUMBER]%. For these format validations I made three functions (starts_with_pm, ends_with_percent_and_numeric, extract_number) which I won't describe here, but you can view that on my GitHub (I also used them via if statements).

To actually change the values I used pactl. It is very simple to use. You can even use @DEFAULT_SINK@ to not bother with getting the current source (sink) that you are making the changes to. Set volume with set-sink-volume and mute with set-sink-mute.

One last thing: just as I mentioned upper if the volume is muted then first I unmute.

I also crafted this into a function:

set_volume() {
  if [[ $1 == "full" ]]; then
    pactl set-sink-mute @DEFAULT_SINK@ toggle
  else
    muted=$(pactl get-sink-mute @DEFAULT_SINK@ | awk '{print $2}')
    if [[ $muted == "yes" ]]; then
        set_volume "full"
    else
        pactl set-sink-volume @DEFAULT_SINK@ "$1%"
    fi
  fi
}
# usage: set_volume ["full" or number]
Enter fullscreen mode Exit fullscreen mode

To make this work via command line and to also minimize and maximize the values I used few if statements:

minvolume=0
maxvolume=150
# ...
volume="$1"
if [[ $volume == "full" ]]; then
  set_volume $volume
  exit
fi
# ...
changeval=$(extract_number "$volume")
currvolume=$(extract_number $(pactl get-sink-volume @DEFAULT_SINK@ | awk '{print $5}'))
finalvolume=$(( $currvolume + $changeval))
if (( $finalvolume < $minvolume )); then # if goes under min then use the min value
  finalvolume=$minvolume
fi
if (( $finalvolume > $maxvolume )); then # if goes over max then use the max value
  finalvolume=$maxvolume
fi
set_volume $finalvolume
Enter fullscreen mode Exit fullscreen mode

Watching for change

Now comes the final part. I start by listening to events via pactl subscribe and via a while loop I display notifications based on if it's muted or what's the current volume percentage.

Because now I will need icons I downloaded 4 types of them: low, mid, high, muted. I also added variables to my script which decides the border limits:

highafter=75
midafter=35

muteimg="$HOME/dotfile-assets/volume-mute.svg"
highimg="$HOME/dotfile-assets/volume-high.svg"
lowimg="$HOME/dotfile-assets/volume-low.svg"
midimg="$HOME/dotfile-assets/volume-mid.svg"

get_icon_from_value() {
  if (( $1 < $midafter )); then
    echo "$lowimg"
  elif (( $1 < $highafter )); then
    echo "$midimg"
  else
    echo "$highimg"
  fi
}
Enter fullscreen mode Exit fullscreen mode

Now I could easily display alerts. The while looks like this:

# ...
pactl subscribe | grep --line-buffered "sink" |
while read; do
  muted=$(pactl get-sink-mute @DEFAULT_SINK@ | awk '{print $2}')
  if [[ $muted == "yes" ]]; then
    show_alert "$muteimg" "0" "Volume: Muted"
  else
    currvolume=$(get_curr_volume)
    icontoshow=$(get_icon_from_value $currvolume)
    show_alert "$icontoshow" "$currvolume" "Volume: $currvolume%"
  fi
done
exit
# ...
Enter fullscreen mode Exit fullscreen mode

Making it work

The script is ready. All that is left is making i3 start the watch on startup and adding keybinds that use the script.

I won't detail them here because i3 has great documentation about it. If you are curious you can view my i3 config to see where I created the keybindings and where I added the autostart.

If you are interested more of this content then I suggest you to read posts from my series about improving my setup and developer productivity.

💖 💪 🙅 🚩
tomoviktor
Tömő Viktor

Posted on June 27, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related