FreeBSD
January 23, 2020

Ricing your *nix desktop | Panel bars I

Knowing information about your system in real time is an expected thing in every computer. Now that we have a desktop we could be "piping" the information through the terminal emulator or we can use a bar to constantly display the wanted feedback from the computer.

Printing information about the system in a terminal emulator it's fine but implies having to launch a terminal each time we want to know the feedback. A panel bar is just a space of the screen dedicated to do so.

You have some choices out there to use in order to get the system info printed and updated. In this guide we're going to use Lemonbar. It's written in C, and it does what it has to do in a clean way.

Grab the package using manager tool. For FreeBSD it's:

$ doas pkg install lemonbar

Although you can end with a complex script with several blocks of code, the way it works is fairly simple: Lemonbar reads information from a script and prints it into a dedicated space.

Let's create our content script inside the .scripts directory and name it status_panel. Don't forget to change permissions in order to allow execution.

$ touch .scripts/status_panel

$ doas chmod u+x .scripts/status_panel

Some basic information that is nice to have are the current time, the network status, the volume level, the disk usage or the battery level and status.

Time and Date

For the time and date, we can create a function inside our status_panel named info_TimeDate() and populate it this way:

# ~/.scripts/status_panel

#!/bin/sh

info_TimeDate()
{
    TTIME=$(date +"%H:%M")
    TDATE=$(date +"%m-%d-%Y")
       
    printf "%s\\n" "$TTIME | $TDATE"
}

Now we need to add some magic to get it printed through Lemonbar. Let's add a loop that rests for a second and updates the information.

# ~/.scripts/status_panel

while true; do
    BAR_INPUT="%{c} TIME: $(info_TimeDate)"
    printf "%s\\n" "$BAR_INPUT"
    sleep 1
done

As you can see we've created a variable named BAR_INPUT that contains a string.

The first block %{c} is an option from Lemonbar that indicates the following content to be aligned to the center.

The middle block TIME: is just plain text and the last block $(info_TimeDate) is a call to our function containing the time and date value.

Battery

To get information about the battery, we can work with some sysctl data:

# ~/.scripts/status_panel

info_Battery()
{
       STATE="$(sysctl hw.acpi.battery.state | awk '{ print $2 }')"

       case $STATE in
               1)
                       OUTPUT="discharging"
                       ;;
               2)
                       OUTPUT="charging"
                       ;;
               7)
                       OUTPUT="no battery"
                       ;;
               *)
                       OUTPUT="ERR"
                       ;;
       esac

       printf "%s\\n" "$OUTPUT"
}

In this case, if we type in the terminal sysctl hw.acpi.battery the results show that battery.state is 1 when not plugged and 2 when plugged to AC. This laptop has a removable battery, so there's a third state, 7 that indicates whether the battery is plugged or not.

The first line of the function gets the information from sysctl and prints only the number which is at the second argument.

We're using AWK which is a standard in Unix like systems and a swiss army knife programming language designed for text processing commonly used as a data extraction and reporting tool.

The second part of the function is a switch-case statement. This is basic in every programming language. Depending on the given variable, the switch-case statement tries to match it with the values given and if no value is equal to our variable, a default case is reached.

In this example, given the value of the battery status we can print if it's charging or not. And if by some mistake the information parsed is not correct or doesn't reach the expected values, we have a default state which prints ERR.

The code for getting info about the battery charge is similar:

# ~/.scripts/status_panel

   CHARGE="$(sysctl hw.acpi.battery.life | awk '{ print $2 }')"

Now we can concatenate the battery status with the battery charge.

# ~/.scripts/status_panel

info_Battery()
{
       STATE="$(sysctl hw.acpi.battery.state | awk '{ print $2 }')"
       CHARGE="$(sysctl hw.acpi.battery.life | awk '{ print $2 }')%"

       case $STATE in
               1)
                       OUTPUT="discharging $CHARGE"
                       ;;
               2)
                       OUTPUT="charging $CHARGE"
                       ;;
               7)
                       OUTPUT="no battery"
                       ;;
               *)
                       OUTPUT="ERR"
                       ;;
       esac

       printf "%s\\n" "$OUTPUT"
}

Network

The network data can be retrieved by ifconfig. Before creating our script we have to run the command once in the terminal to get the data values we need. After doing so we can edit our status_panel script.

# ~/.scripts/status_panel

info_NetworkStatus()
{
       WIFI_INFO=$(ifconfig wlan0)
       WIFI_STATUS=$(printf "%s\\n" "$WIFI_INFO" | grep -w "status:" | awk '{ print $2 }')
       SSID=$(printf "%s\\n" "$WIFI_INFO" | grep -w "ssid" | awk '{ print $2 }')

       ETH_INFO=$(ifconfig em0)
       ETH_STATUS=$(printf "%s\\n" "$ETH_INFO" | grep -w "status:" | awk '{ print $2 }')

       if [ "$WIFI_STATUS" = "associated" -a "$ETH_STATUS" = "no" ]
       then
               printf "%s\\n" "${SSID}"
       elif [ "$ETH_STATUS" = "active" ]
       then
               printf "%s\\n" "Wired"
       else
               printf "%s\\n" "Down"
       fi     
}

In this laptop's particular case the WiFi is located by ifconfig with the name wlan0 and the Ethernet is named em0. Check yours to avoid errors while writing your script.

The first five variables get the necessary information about WiFi and Ethernet, trimming it with grep and awk.

The main part of the function is a conditional block using if statements. In other programming languages we write == to compare values; in sh is just one = sign. The -a operator is the same as the && operator in C, which stands for AND so both conditions have to be met in order to execute the statement's code.

Back into the function, the first condition checks if WiFi is up and associated to a SSID and the Ethernet isn't plugged. If so the information displayed is the SSID name.

The second condition evaluates if the Ethernet port is plugged in. If so the information displayed changes to "Wired".

Finally if no conditions are met, the displayed information changes to "down".

Audio

Volume information is managed by mixer.

# ~/.scripts/status_panel

info_Volume()
{
   VOL="$(mixer | grep vol | awk '{ print $7 }' | grep -o '[^:]*')"
   printf "%s\\n" "${VOL}%"
}

In this case when we type mixer in the terminal we can see a more complete information list about the sound card. By adding a pipe with grep vol we retrieve only the volume value. The next pipe using awk gets rid of the rest of the string since mixer vol gives us a long string like this:

Mixer vol     is currently set to 85:85

After the awk pipe we have the xx:xx value. For studio and audio production you maybe want to have both channels value printed, but in most of the cases since the value is going to be the same for both left and right, we can pass a last pipe with a regular expression to get only a single final value. This is what grep -o '[^:]*' regex does.

RAM memory

The program top gives us real time info about RAM usage among many more things. Type $ top -n in order to get a check about what info you can get using it. Mem is the name of the line we are looking for.

Mem: 1191M Active, 325M Inact, 65M Laundry, 810M Wired, 5399M Free

Let's try to get an average percentage about our used memory:

# ~/.scripts/status_panel

info_RAM()
{
    USEDRAM=$(top | grep -w "Mem" | awk '{ print $2+$4+$6+$8 }')
    TOTALRAM=$(dmesg | grep -E '^avail memory' | cut -d'(' -f2 | cut -d')' -f1 | awk '{ print $1 }')
    PRCNTUSED=$(awk -v u=$USEDRAM -v t=$TOTALRAM 'BEGIN{print 100 * u / t}' | awk -F. '{ print $1"."substr($2,1,2) }')


    printf "%s\\n" "${PRCNTUSED}%"
}

It may seem complicated but it's only tricky. Let's take a look at the process.

— The variable USEDRAM gets a number based on the information displayed in the line Mem from the top command ( top | grep -w "Mem" ). The number has to be a sum of the non-free RAM Megabytes so we get all together using awk.

— The variable TOTALRAM gets the available memory in the system. If we type $ dmesg | grep memory we should get at least two values, one for the real memory and another one for the available memory:

$ dmesg | grep memory
real memory = 8589934592 (8192 MB)
avail memory = 8128942080 (7752 MB)

In this case we want to work with the available memory so we can get it using grep -E '^avail memory'. The next pipe in the variable is used to remove everything but the number expressed in MB, achieved with cut. The third pipe gets only the number expressed in MB without the "MB" word.

— The variable PRCNTUSED is the basic formula to compare both numbers and determine the percentage used. We are using awk to achieve it and in order to pass variables to awk we need to tell it before the calculations. -v x=$y is the way to define a variable inside awk. In this case we have more than one so we repeat the step for both our used and available RAM.

The next words in the line are our percentage formula using the defined variables.

Lastly we have a pipe that again uses awk to leave only two decimals to our percentage result.

CPU load

The program top also gives us information about the CPU usage. If we type $ top -n we can search for the line starting with CPU: .

CPU: 3.6% user, 0.0% nice, 1.5% system, 0.7% interrupt, 94.3% idle

If we want to know the CPU load we only need to sum the first four values.

# ~/.scripts/status_panel

info_CPU()
{
       USEDCPU=$(top -n | grep -w "CPU" | awk '{ print $2+$4+$6+$8 }')
       printf "%s\\n" "${USEDCPU}%"
}

Disk space

Using the command df we can get disk usage and free space information.

After running the commands, we have interest in the following data:

Filesystem        1K-blocks   Used    Avail Capacity Mounted on
zroot/ROOT/default 109918500 4539448 105379052    4%   /

That's the information about our main disk so in our script we can write a function to fetch it:

# ~/.scripts/status_panel

info_freeSpaceHDD()
{
    AVAIL=$(df -H / | grep -w "default" | awk '{ print $4 }')
    printf "%s\\n" "$AVAIL"
}

Adding the flag -H after the command df translates the shown data to "human readable" data so we can get how many free GB or MB we have.

We use grep to get only the line containing zroot/ROOT/default and we use awk to get the fourth argument which is the available space.

Running it all together

Now that we have all our functions written and working it's time to update our while loop in the script.

Lemonbar uses {l} {c} {r} respectively to align items to the left, center and right parts of the panel bar.

# ~/.scripts/status_panel

while true; do
       BAR_INPUT="%{l} CPU: $(info_CPU) RAM: $(info_RAM) HDD: $(info_DriveSpace) %{c}$(info_TimeDate) %{r} N: $(info_NetworkStatus) V: $(info_Volume) B: $(info_Battery)"
   printf "%s\\n" "$BAR_INPUT"
   sleep 1
done

Write it and run it from a terminal instance writing the following:

$ .scripts/status_panel.sh | lemonbar

And now you have a great custom top panel bar that displays updated info about your system.

Summing up

This code is intended to work inside FreeBSD so expect some re-work if you are under Linux. Some expressions can be achieved using a shell expr or using bc in a cleaner way, but I tried not to introduce or mix too many content. Just try to rewrite some expressions without awk using them.

We'll explore in the next episode how to automate and how to make our bar more efficient and how to add colors and custom fonts to it. So far here's the complete script we've been writing:

# ~/.scripts/status_panel

#!/bin/sh
info_Battery()
{
       STATE="$(sysctl hw.acpi.battery.state | awk '{ print $2 }')"
       CHARGE="$(sysctl hw.acpi.battery.life | awk '{ print $2 }')%"

       case $STATE in
               1)
                       OUTPUT="discharging $CHARGE"
                       ;;
               2)
                       OUTPUT="charging $CHARGE"
                       ;;
               7)
                       OUTPUT="no battery"
                       ;;
               *)
                       OUTPUT="ERR"
                       ;;
       esac

       printf "%s\\n" "$OUTPUT"
}

info_NetworkStatus()
{
       WIFI_INFO=$(ifconfig wlan0)
       WIFI_STATUS=$(printf "%s\\n" "$WIFI_INFO" | grep -w "status:" | awk '{ print $2 }')
       SSID=$(printf "%s\\n" "$WIFI_INFO" | grep -w "ssid" | awk '{ print $2 }')

       ETH_INFO=$(ifconfig em0)
       ETH_STATUS=$(printf "%s\\n" "$ETH_INFO" | grep -w "status:" | awk '{ print $2 }')

       if [ "$WIFI_STATUS" = "associated" -a "$ETH_STATUS" = "no" ]
       then
               printf "%s\\n" "${SSID}"
       elif [ "$WIFI_STATUS" = "associated" -a "$ETH_STATUS" = "active" ]
       then
               printf "%s\\n" "Wired"
       else
               printf "%s\\n" "Down"
       fi     
}

info_Volume()
{
       GETVOL="$(mixer | grep vol | awk '{ print $7 }' | grep -o '[^:]*')"
       printf "%s\\n" "${GETVOL}%"
}

info_TimeDate()
{
       TTIME=$(date +"%H:%M")
       TDATE=$(date +"%m-%d-%Y")
       
       printf "%s\\n" "$TTIME | $TDATE"
}

info_CPU()
{
       USEDCPU=$(top -n | grep -w "CPU" | awk '{ print $2+$4+$6+$8 }')
       printf "%s\\n" "${USEDCPU}%"
}

info_RAM()
{
       USEDRAM=$(top -n | grep -w "Mem" | awk '{ print $2+$4+$6+$8 }')
       TOTALRAM=$(dmesg | grep -E '^avail memory' | cut -d'(' -f2 | cut -d')' -f1 | awk '{ print $1 }')
       PRCNTUSED=$(awk -v u=$USEDRAM -v t=$TOTALRAM 'BEGIN{print 100 * u / t}' | awk -F. '{ print $1"."substr($2,1,2) }')

       printf "%s\\n" "${PRCNTUSED}%"
}


info_DriveSpace()
{
       AVAIL=$(df -H / | grep -w "default" | awk '{ print $4 }' | awk -F'[A-Z]' '{print $1}')
       TOTAL=$(df -H / | grep -w "default" | awk '{ print $2 }' | awk -F'[A-Z]' '{print $1}')
       
       printf "%s\\n" "${AVAIL}GB"
}


while true; do
       BAR_INPUT="%{l} CPU: $(info_CPU) RAM: $(info_RAM) HDD: $(info_DriveSpace) %{c}$(info_TimeDate) %{r} N: $(info_NetworkStatus) V: $(info_Volume) B: $(info_Battery)"
   printf "%s\\n" "$BAR_INPUT"
   sleep 1
done

Go tweak it as your wish (: