Automatic Visual Feedback for System Volume Change in I3wm via Dunst
Tags: linux, i3, dunst, dunstify
Published: 2024-06-27 17:39:46

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](https://i3wm.org/). 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](https://en.wikipedia.org/wiki/Z_shell) script. If you are familiar with linux scripting you may ask: why didn't I use [bash](https://en.wikipedia.org/wiki/Bash_(Unix_shell))? 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`](https://github.com/11Firefox11/.dotfiles/blob/e3ef385b969f1d5a0a9f81ffcf3ac0c057697eb1/bin/.local/scripts/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:
```zsh
volume-changer "watch" 
```

Increase or decrease the volume or mute it fully (which automatically toggles):
```zsh
volume-changer "+5%"
volume-changer "+50%"
volume-changer "-5%"
volume-changer "-21%"

volume-changer "full"
```

My script also logs details via [`logger`](https://man7.org/linux/man-pages/man1/logger.1.html) 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](https://github.com/11Firefox11/.dotfiles/blob/e3ef385b969f1d5a0a9f81ffcf3ac0c057697eb1/bin/.local/scripts/change-volume). I made it so that in the [top few lines](https://github.com/11Firefox11/.dotfiles/blob/e3ef385b969f1d5a0a9f81ffcf3ac0c057697eb1/bin/.local/scripts/change-volume#L2-L12) you can easily configure few basic things. Don't forget to download the [icons](https://github.com/11Firefox11/.dotfiles/tree/e3ef385b969f1d5a0a9f81ffcf3ac0c057697eb1/assets/dotfile-assets) too if you need them.

## The code
### Sending notifications
The base of all of this is notifications. Because my i3 came with [dunst](https://dunst-project.org/) and I liked the simple look of it I decided to use it as the notification [daemon](https://en.wikipedia.org/wiki/Daemon_(computing)). 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](https://linuxcommandlibrary.com/man/dunstify). I also found a [nice website that uses examples to show how it works](https://web.archive.org/web/20240402025250/https://smarttech101.com/how-to-send-notifications-in-linux-using-dunstify-notify-send/).

> Important note is that the progress bar feature is available since [dunst version 1.6.0](https://github.com/dunst-project/dunst/releases/tag/v1.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](https://github.com/dunst-project/dunst/issues/1321).

I created a perfect function for showing alerts:
```zsh
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]
```

### 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](https://github.com/11Firefox11/.dotfiles/blob/e3ef385b969f1d5a0a9f81ffcf3ac0c057697eb1/bin/.local/scripts/change-volume#L15-L42) (I also used them via if statements).

To actually change the values I used [`pactl`](https://linux.die.net/man/1/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:
```zsh
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]
```

To make this work via command line and to also minimize and maximize the values I used few if statements:
```zsh
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
```

## 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](https://github.com/11Firefox11/.dotfiles/tree/e3ef385b969f1d5a0a9f81ffcf3ac0c057697eb1/assets/dotfile-assets): low, mid, high, muted. I also added variables to my script which decides the border limits:
```zsh
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
}
```

Now I could easily display alerts. The while looks like this:
```zsh
# ...
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
# ...
```

## Making it work
The script is ready. All that is left is [making i3 start the watch on startup](https://i3wm.org/docs/userguide.html#_automatically_starting_applications_on_i3_startup) and [adding keybinds that use the script](https://i3wm.org/docs/userguide.html#keybindings). 

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](https://github.com/11Firefox11/.dotfiles/blob/e3ef385b969f1d5a0a9f81ffcf3ac0c057697eb1/i3/.config/i3/config#L25-L29) and where [I added the autostart](https://github.com/11Firefox11/.dotfiles/blob/e3ef385b969f1d5a0a9f81ffcf3ac0c057697eb1/i3/.config/i3/config#L213).

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](https://tomoviktor.com/series/developer-productivity/page/1/).