How To: Refresh your status bar in response to a hotkey - Desktop Customization & Workflow

Users browsing this thread: 1 Guest(s)
mort
Members
(This post tries to explain how every part works in some detail. If you're just interested in code to copy/paste, look at the end of the post.)

A lot of you are probably using status bars where the actual status bar process just essentially renders the text printed by a script. I know at least both swaybar and i3bar work like that. A simple status script would be this:

Code:
update() {
    echo \
        "VOL:$(pactl list sinks \
            | grep "^\s*Volume:" | cut -d/ -f2 \
            | tr -d '\n' | sed 's/\s\s*/ /g')|" \
        "$(date -Iminutes | sed 's/T/ /; s/+.*//')"
}

while :; do update; sleep 5; done

That works well enough, it shows the current volume of your audio devices and shows the current date. However, it's not great; it only updates once every 5 seconds, which makes it annoying to use to adjust your volume. You could decrease the update interval, but it would be stupid to have a status script running every 100ms when nothing almost ever changes.

One decent solution would be to make the status script update after your
Code:
pactl set-sink-volume @DEFAULT_SINK@ +10%
hotkey runs. We can use Unix signals to achieve this.

A signal is basically one of the ways a Unix-like kernel (like Linux) can use to communicate with its processes. The kernel sends a SIGSEGV signal ("Segmentation Fault") when the application tries to access memory it's not supposed to, it sends a SIGWINCH signal when the terminal window's dimensions changes, it sends a SIGINT signal when you interrupt the process (such as by hitting ctrl-c in a terminal), it sends a SIGCHLD signal when a child process terminates. Processes can install signal handlers, which is code which is executed when the process receives a signal.

Gracefully, the designers of Unix gave us two signals which don't have any semantic meaning in and of themselves; SIGUSR1 and SIGUSR2. The purpose of these signals is to trigger application-specific behavior. We can make the script call the update function again when it receives the SIGUSR1 signal:

Code:
update() {
    echo \
        "VOL:$(pactl list sinks \
            | grep "^\s*Volume:" | cut -d/ -f2 \
            | tr -d '\n' | sed 's/\s\s*/ /g')|" \
        "$(date -Iminutes | sed 's/T/ /; s/+.*//')"
}

trap true USR1
while :; do update; sleep 5 & wait $!; done

This does two things: it "traps" the SIGUSR1 signal (this gets rid of the default SIGUSR1 behavior, which is to exit the process), and it replaces the sleep 5 with sleep 5 & wait $!. The meaning of sleep 5 & wait $! is basically: "Spawn a new background child process (sleep 5), and wait for it to exit", but the crucial difference is that the "wait" command is aborted when a signal is received. (The $! variable always refers to the previous background process that was spawned, in this case the sleep process.)

So now, when we send our script a SIGUSR1 signal, it will instantly print a new status line. Now we need to run
Code:
kill -USR1 <our script's process ID>
in response to the volume keys. In order to do that, there has to be a way to find the process ID of the status script. One way to do that is to use a PID file; the status script just has to write its PID to some defined location, such as /tmp/status-$USER.pid:

Code:
update() {
    echo \
        "VOL:$(pactl list sinks \
            | grep "^\s*Volume:" | cut -d/ -f2 \
            | tr -d '\n' | sed 's/\s\s*/ /g')|" \
        "$(date -Iminutes | sed 's/T/ /; s/+.*//')"
}

echo "$$" > "/tmp/status-$USER.pid"
trap true USR1
while :; do update; sleep 5 & wait $!; done

The $$ variable always refers to the PID of the currently executing script.

Now, everything that remains is to add
Code:
kill -USR1 "$(cat "/tmp/status-$USER.pid")"
to your volume change hotkeys. For example, the volume up hotkey could run:
Code:
pactl set-sink-volume @DEFAULT_SINK@ +10% && kill -USR1 "$(cat "/tmp/status-$USER.pid")"

Here's my status script, which uses something like what's described here to update in response to my hotkeys: https://github.com/mortie/dots/blob/mast.../status.sh
neeasade
Grey Hair Nixers
Hey mort,

Thanks for sharing, it's easy to forget how many moving parts there are when you want something that can feel 'simple' on these systems. I like the trap wrapper for just passing in "reload" to your script.

For what it's worth I currently use i3block[1] as a statusbar manager, which has a configuration option for associating signals with 'blocks' to do a refresh -- I wonder how many of these "status bar managers" support interrupts like this

[1] https://github.com/vivien/i3blocks
Ramiferous
Nixers
Big thanks Mort for this write up. I will certainly be playing around with it so that my spectrwm baraction.sh script isn't calling wttr.in EVERY SECOND! lol. For real tho, I've disabled it for now and have pushed it back to reload every 10 seconds (I hardly adjust the volume anyway).

While I am a complete novice when it comes to writing scripts (or any code for that matter) I have been able to get by for several years just tinkering and tweaking scripts I find on the web and learning as I go. You've explained the functions well in this write up which makes me feel like I've learned something so cheers!
_____________________________________________________________________________________________
“Maybe you have some bird ideas... Maybe that's the best you can do.” - Terry A. Davis (R.I.P Percival)
s0kx
Members
This is great. I'm not as smart as mort, but I've tried to combat this same problem using fifos. Something like:
Code:
tail -n1 -F FIFO | while read vol; do
    echo "Volume: $vol"
done
Then setup your hotkeys so that they also echo the volume to the fifo.

Can't be worse than checking the volume every second right??
Ramiferous
Nixers
Ok, so I've tried but I cannot get this working. My config is below:

PHP Code:
#!/bin/sh                                                                                                 

Volume() {
    
VolInf="$(mixerctl outputs.master | sed -e 's|.*,||g')"
    
echo "Vol $name $(expr \( $VolInf \* 100 \) / 254)%"
}

Battery() {
    
Capacity="$(envstat -s acpibat0:charge | tail -1 | sed -e 's,.*(\([ ]*[0-9]*\)\..*,\1,g')%"
    
State="$(envstat -d acpibat0 | awk 'FNR == 10 {print $2}')"

    
if [ "${State}"TRUE" ]; then
        
echo 'Charging' ${Capacity}
    else
        echo 
'Discharging' ${Capacity}
    
fi
}

Weather() {
    echo 
"$(curl -s wttr.in/Melbourne?format='%C+%t\n')"
}

Pkgs() {
    echo 
"$(pkg_info | wc -l | sed -e 's/^[ \t]*//') pkgs"
}

Update() {
    echo \
        
"$(Weather) | $(Pkgs) | $(Battery) | $(Volume)" &
    
wait
}

pidfile="/tmp/status-$USER.pid"

if [ "$1" "reload" ]; then
 kill 
-USR1 "$(cat "$pidfile")"
 
exit $?
fi

if [ "$1" != "no-pidfile" ]; then
 
echo "$$" "$pidfile"
fi
trap true USR1

while :
do
    
Update
    sleep 60 
&
    
wait $!
done 

I've also added this to my .spectrwm.conf

PHP Code:
# Volume Keys
program[raise_volume]   = mixerctl -w outputs.master+=12 && kill -USR1 "$(cat "/tmp/status-$USER.pid")"       
bind[raise_volume]      = XF86AudioRaiseVolume
program
[lower_volume]   = mixerctl -w outputs.master-=12 && kill -USR1 "$(cat "/tmp/status-$USER.pid")"
bind[lower_volume]      = XF86AudioLowerVolume 
_____________________________________________________________________________________________
“Maybe you have some bird ideas... Maybe that's the best you can do.” - Terry A. Davis (R.I.P Percival)
vain
Long time nixers
The version without a PID file:

Code:
#!/bin/sh                                                                                                

Volume() {
    ...
    # they all stay the same
    ...
}

...

Update() {
    echo "$(Weather) | $(Pkgs) | $(Battery) | $(Volume)"
}

trap true USR1

while :
do
    Update
    sleep 60 &
    wait $!
done

Let’s say, you saved this as "update_bar.sh". Your key binds would then look like this:

Code:
# Volume Keys
program[raise_volume] = mixerctl -w outputs.master+=12 && pkill -USR1 '^update_bar.sh$'
...

Saves you the trouble of dealing with a PID file, which can contain a wrong PID. This version uses "pkill" to scan through all running processes and send the signal to the ones matching the pattern.

(This still assumes that spectrwm runs key binds using "sh -c ...". Don’t know if that’s true.)
Ramiferous
Nixers
Yeah, this actually makes sense to me. I will try this later tonight and let you know if it worked. Cheers
_____________________________________________________________________________________________
“Maybe you have some bird ideas... Maybe that's the best you can do.” - Terry A. Davis (R.I.P Percival)
Ramiferous
Nixers
So it would appear that this does not work for me... Perhaps because the bar.sh is called by spectrwm itself and is only reloaded when the wm itself is reloaded? I'll try and find a way around it.
_____________________________________________________________________________________________
“Maybe you have some bird ideas... Maybe that's the best you can do.” - Terry A. Davis (R.I.P Percival)