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 (: