Ricing your *nix desktop

FreeBSD gearing-up

Index


Desktop environments are just a pre-packed bunch of software from which you will maybe need a small part for your daily usage. They are great for users that just want to start using the OS, but if you want more control on what you install, what you use, and how it behaves then it has to be done by hand.

The term “ricing” was inherited from the practice of customizing cheap Asian import cars to make them appear to be faster than they actually were.

We are not going to make anything appear more of what it is but choose the exact amount of software needed to cover our needs, and eventually making it look cool.

The Ricing recipe

Just like a recipe, the first thing is getting the required ingredients.

For a basic desktop setup we need the following:

There are a lot more options for each part, just search inside your package manager to get a list of all the available ones for your operating system. You can also search GitHub or similar pages for even more alternatives.

If you find your desktop environment has a style or tools you don't want to loose, you can also select the single packages coming from a Desktop Environment and set up your custom DE flavor that fit your needs, without the extra fat.

How to cook it

All the items from the recipe list are kind of unrelated to each other as they have their own unique function to achieve. The cool thing is that given this level of modularity you can tweak and change almost any part of your desktop independently.

Although some programs require to change the source code and compile them again like the suckless tools, most programs allow the user to tweak them in configuration files after installing them. Each program uses it's own syntax for understanding configuration content, but they're all readable text files. Those config files are the ones named dotfiles.

dotfiles due to local configuration files names start with a dot (.)

Dotfiles are usually separated from the program's directory. There's no “official rule” for where to place the dotfiles. Some programs need their own path to read their configuration files but for general purposes, we can organize our $HOME directory like this to maintain an order structure:

├── .config
├── .scripts
├── .fonts
└── workspace
    ├── documents
    ├── images
    ├── downloads
    ├── music
    └── ...

Typically configuration files for most of the programs lie in the .config directory and the custom fonts that we may use to change the look of the desktop are inside the .fonts directory.

The .scripts directory is the place to store all our custom scripts. They may be used in X daemons to perform actions or just by executing them trough the terminal emulator.

What to expect

This example guide on building a desktop over a Window Manager is going to be explained with the following programs' combination: bspwm, urxvt, lemonbar, vim and nnn

A complete repository with a ready-to-go FreeBSD window manager config based on this guides can be found here.

As mentioned above, almost everything is modular so you can opt to install different components.

Window managers

Window managers allow you control where to place frames (or windows) and how they look in a graphical user interface. The two main classes of window managers are tiling window managers and floating window managers.

Since everyone has different preferences and workflows, choosing a window manager should be a personal decision made upon your needs.

In the case of this guide we use bspwm(1), which is a tiling window manager with options to make floating frames that don't stack in the tiling layout.

bspwm config by u/termwiz @ github

Getting a Display Server

Right now we don't have any desktop environment or window manager installed, after a clean installation we should be looking at the tty (the computer terminal). In order to reach our desktop we need to install a display server, named xorg.

There is also a more modern display server made by the same devs named wayland. It works a bit different so it will be covered separately.

X.org provides a complete implementation of the X Window System (X11) and is widely used in the *nix world.

In most cases your system is going to have xorg in their resources, so you should be able to install by just typing:

$ pkg install xorg

For extra functionality, you can install the following packages if your system's xorg main package doesn't have them included:

$ pkg install xloadimage xsetroot xrdb

In our $HOME directory we need to create two dotfiles to configure the x system. The first one is .xinitrc and the second one is .Xresources.

$ touch .xinitrc .Xresources

The .xinitrc file launches the desktop. It's a simple script called by the startx command. The .Xresources file is quite self-explanatory. It contains settings for parts of the window manager.

Let's edit the file (from the tty you can run vi, vim, nano(if in linux) or ee(if in BSD)).

Note that your .xinitrc file will be empty.

#!/bin/sh
# ~/.xinitrc

# load .Xresources
xrdb -merge ~/.Xresources

# set background wallpaper (if wanted)
xsetbg -fullscreen /path/to/your/wallpaper.png &

# set default cursor
xsetroot -cursor_name left_ptr &

# start the simple X hotkey daemon
sxhkd &

# launch status bar

# launch a terminal by default

# spawn window manager
exec bspwm

We didn't fill all the comments (lines with hastahgs except the first one, are comments in shell scripting) in the file, but we'll come to those later.

Installing bspwm

Right now we cannot launch our desktop yet since the packages for the hotkey daemon and the window manager aren't installed. Most of the nix-like OS derivations have bspwm included in their packages' repo so you should be able to install it just straight.

$ pkg install bspwm sxhkd

It requires some dependencies to work, specially note sxhkd. Your package manager should take care of the dependencies. If you opt to install bspwm from source you can look into the Makefile for the needed libs, or look into the make log if installation stops during the process.

bspwm relies in sxhkd to handle keyboard and pointer inputs.

Configuring bspwm

After the installation, we need to create two sub-directories in our .config directory; one for bspwm and another one for sxhkd.

$ mkdir -p .config/bspwm && mkdir -p .config/sxhkd

Inside .config/bspwm we need to create the configuration file for bspwm which is an executable shell script named bspwmrc. In order to make it executable we need to add the flag x with chmod.

The following line creates the config file, marks it as executable, and opens it in the vim editor:

$ touch .config/bspwm/bspwmrc && chmod +x "$_" && vim "$_"

Once we are in, the configuration of bspwm(1) only requires a few inital client settings rules. The rest can be added as desired by reading its man page.

First line of the file has to be the shell shebang (#!/bin/sh) since it's a shell script.

— For the client, we can define how many desktops we want in a monitor. The following example adds four desktops on the monitor.

bspc monitor -d I II III IV

— If we want to control the window's borders settings like color, width we can add the following:

bspc config border_width 1
bspc config normal_border_color #f0f0f0
bspc config active_border_color #ffffff

— To make the cursor interact with the window instances we can play with focusing on cursor hover, cursor click, using corners plus cursor clicks to move or to resize, etc:

bspc config focus_follows_pointer   true
bspc config pointer_follows_monitor true
bspc config pointer_modifier        mod1
bspc config pointer_action1         move
bspc config pointer_action2         resize_side
bspc config click_to_focus          any
bspc config swallow_first_click     false

— We can also make rules for certain programs, instructing them to launch at a specific monitor, or launching them in floating mode instead of tiling mode for example.

bspc rule -a Firefox desktop='^4' state=floating follow=on
bspc rule -a Telegram-desktop desktop='^2'
bspc rule -a feh state=floating

A basic bspwmrc file configuration from its original author can be found here, and the one used in this series can be found at this link.

I encourage you to tweak the configuration file in depth by reading the examples and the man page, to fit your needs and workflows.

bspwm config by u/LukasDrsman @ r/unixporn

Configuring sxhkd

sxhkd(1) is a daemon always listening to our keyboard, and executing commands based on the input. As we mentioned earlier, bspwm(1) relies on sxhkd(1) to manage interaction from keyboard.

The same way we need a configuration file for sxhkd(1). Inside .config/sxhkd create a file named sxhkdrc.

$ touch .config/sxhkd/sxhkdrc && vim "$_"

The configuration of sxhkd(1) commands is based on the following syntax:

key combo
    commad to execute

In order to avoid use of common keys to trigger command execution, is common to add the super key at the beginning of the key combo.

The super key by default refers to the key launcher from your keyboard, usually the one with the Windows or MacOs logo.

Launching programs with sxhkd

When using a tiling window manager, icons and mice cursor aren't something that useful. But we still need to launch programs somehow.

— To launch a terminal emulator for example, we can setup the instruction as follows:

super + Return
    urxvt

that is, when pressing both super and Return keys, a new urxvt(1) will launch.

Although having some shortcuts to launch programs is fine, filling up all the keys of the keyboard to launch a program may not be the best solution to have a functional and comfy workplace. We will add a program launcher like dmenu or rofi to handle that task.

A good idea is to define a key combo to launch the program launcher:

# program launcher
super + @space
    #program_launchers_name

Controlling bspwm through sxhkd

We can go a bit further and setup some logic to manage bspwm(1) windows and nodes.

— In order to swap window states via key-combo we can do the following:

# set the window state
super + {t,shift + t,s,f}
    bspc node -t {tiled,pseudo_tiled,floating,fullscreen}

— Navigation between nodes in a desktop can be mapped in keys similar to Vim's ones, so we can focus the desired node:

# focus the node in the given direction
super + {_,shift + }{h,j,k,l}
    bspc node -{f,s} {west,south,north,east}

— The same way we can specify the direction for a new program to allocate in the desktop

# preselect the direction
super + ctrl + {h,j,k,l}
    bspc node -p {west,south,north,east}

— We can switch between desktops, shifting between previous and next ones.

# focus the next/previous node in the current desktop
super + {_,shift + }c
    bspc node -f {next,prev}.local

— In a more complex way, we can mix several actions that take the same parameters, and decide the final execution based on the keys pressed:

# focus or send to the given desktop
super + {_,shift + }{1-9,0}
    bspc {desktop -f,node -d} '^{1-9,10}'

In this case we can whether jump into the desired desktop by not pressing the shift key, or send the focused node to the desired desktop if shift is pressed.

— Last but not least we can control window movement and resizing using some key combos.

# expand a window by moving one of its side outward
super + alt + {h,j,k,l}
    bspc node -z {left -20 0,bottom 0 20,top 0 -20,right 20 0}

# contract a window by moving one of its side inward
super + alt + shift + {h,j,k,l}
    bspc node -z {right -20 0,top 0 20,bottom 0 -20,left 20 0}

# move a floating window
super + {Left,Down,Up,Right}
    bspc node -v {-20 0,0 20,0 -20,20 0}

A basic sxhkdrc file configuration from its original author can be found here, and the one used in this series can be found at this link.

Terminal emulators

Terminal emulators allow us access the command line without the need of being in a tty environment, which is required when working in a graphical environment like X11.

The terminal emulator used for this guide is the rxvt-unicode but almost every terminal emulator follows the rule of being tweaked inside a config file.

Installing a terminal emulator

In the previous episode we set up xorg and the bspwm(1) window manager. All the interaction has been done from the tty until now but in order to communicate with the computer once we start our x-session, we need a terminal emulator program that gives us access to the shell.

Before doing any other thing, grab the terminal emulator from your package manager.

$ doas pkg install urxvt

Once it's installed we can add its name to our previous dotfiles.

At the .xinitrc file we can choose whether to add the instruction to launch a terminal-emulator by default:

# ~/.xinitrc

# launch a terminal by default
urxvt &

or to add a daemon launcher for our terminal emulator. This second option makes each new terminal emulator instance a thread under a main process.

# ~/.xinitrc

#launch a terminal emulator daemon
urxvtd -q -f -o &

At the sxhkdrc file we can choose whether to launch a new terminal emulator process or a new terminal emulator client at the terminal emulator instruction:

If you choose to launch a terminal emulator client, type urxvtc instead.

# ~/.config/sxhkd/sxhkdrc

# terminal emulator
super + Return
    urxvt

Now we can jump into our desktop (finally!) and start configuring some more things. In the tty type:

$ startx

And if everything went right, you should have an annoying white terminal being drawn at your screen, like this:

Ricing your Terminal Emulator

The terminal emulator is going to be the most used program in our custom desktop, specially with tiling window managers. It's important to make our terminal emulator a comfy place so let's fix its defaults.

Previously we've created a dotfile in our $HOME directory named .Xresources. This file is the main tool to tweak your terminal emulator.

Since the configuration of a terminal emulator can end up being a long list of options and you maybe opt for adding more than one terminal, or redirect other programs to read the .Xresources file for configuration, let's keep it tidy since the beginning.

Remember that directory structure from the first chapter? We created a directory named .config. Let's make a file inside there named urxvt and open it to add content.

Just for refreshing, type your text editor's name followed by the file's path in order to open it. $ ee .config/urxvt.

Here's an example of a basic urxvt(1) dotfile:

! ~/.config/urxvt

URxvt.termName:         rxvt-256color
URxvt.cursorBlink:      true
URxvt.scrollBar:        false
URxvt.internalBorder:   8
URxvt.font:             xft:Hack:size=10, Hack Regular:style=Regular
URxvt.boldFont:         xft:Hack:size=10, Hack:style=Bold
URxvt*background:       #000000
URxvt*foreground:       #ffffff

There are some parameters that can be shared with other programs, like fonts and colors. Let's split things a bit more and actually make a separate file for both the color and the font values in the .config directory.

Ricing Terminal Emulator Colors

$ touch .config/colors

Although it's supposed we have 256 colors available we only use 16 colors to rice our terminal emulators' style, plus 3 special (background, foreground and cursorColor).

       black  red  green  yellow  blue  magenta  cyan  white
dark      00   01     02      03    04       05    06     07  
light     08   09     10      11    12       13    14     15

Colors from 00 - 07 are used for regular text and colors from 08 - 15 are used for bold text.

The color values are hexadecimal numbers. Jump into the colors' file and fill up with something similar to this:

! ~/.config/colors

! nordic theme
! special
*.foreground: #d8dee8
*.background: #2f343f
*.cursorColor: #b48ead

! black
*.color0 : #4b5262
*.color8 : #434a5a

! red
*.color1 : #bf616a
*.color9 : #b3555e

! green
*.color2 :  #a3be8c
*.color10 : #93ae7c

! yellow
*.color3 :  #ebcb8b
*.color11 : #dbbb7b

! blue
*.color4 :  #81a1c1
*.color12 : #7191b1

! magenta
*.color5 :  #b48ead
*.color13 : #a6809f

! cyan
*.color6 :  #89d0bA
*.color14 : #7dbba8

! white
*.color7 :  #e5e9f0
*.color15 : #d1d5dc

We have to remove our color values from the urxvt config file and include this new color reference. Including resources from a different file follows a C-like syntax:

 #include "<path>"

On the very top of our urxvt file type:

#include "colors"

Open the .Xresources file and write this at the beginning of the file:

! ~/.Xresources

#include ".config/urxvt"

Ricing Terminal Emulator Fonts

With the fonts configuration it's pretty much the same. Let's create a fonts config file inside the .config directory and open it:

$ vim .config/fonts

There are two types of fonts available to use: xft fonts and bitmap fonts.

Regardless of most posts on the net, bitmap fonts can look awesome (like the example image above).

xft fonts follow the next syntax scheme to be defined:

xft:<font_name>:size=<font_size>

bitmap fonts have a longer syntax scheme:

-fndry-fmly-wght-slant-sWdth-astyl-pxlsz-ptSz-resx-resy-spc-avgWdth-rgstry-encdng

Inside our fonts config file let's define some variables to use in an xft font:

! ~/.config/fonts

! remember to install the font first (in this example the font name is "hack")
define fontName Hack
define fontSize 9

define urxvtFontRegular xft:fontName:size=fontSize, fontName Regular:style=Regular

define urxvtFontBold xft:fontName:size=fontSize, fontName:style=Bold

! this variables set antialias and dpi for xft fonts
Xft.antialias 1
Xft.dpi       96

In our urxvt config file we have to tell the terminal emulator to look for this variables in order to print our font. Open the file and change the values:

! ~/.config/urxvt

#include "colors"
#include "fonts"

URxvt.termName:         rxvt-256color
URxvt.cursorBlink:      true
URxvt.scrollBar:        false
URxvt.internalBorder:   8
URxvt.font:             urxvtFontRegular
URxvt.boldFont:         urxvtFontBold

Summing Up

This way each time you need to change any value, try a new color scheme or a new font, things are in the right place and only need to be changed once. Now in order to update our .Xresources file to read the colors, the fonts and the terminal emulator config we need to update .Xresources using xrdb, the X server resource database.

$ xrdb -load ~/.Xresources

After typing the instruction close the actual terminal emulator and launch a new one to see the changes.

Panel bars

Knowing information about your system in real time is an expected thing in every computer. We can pipe the information through the terminal emulator asking to sysctl(8) about our hardware, or now that we have a Desktop, 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.

Installing a Panel Bar

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

Setting up 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
$ chmod +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.

Getting Time and Date

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

#!/bin/sh

info_time_date() {
    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.

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_time_date) is a call to our function containing the time and date value.

Getting Battery Information

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

$ sysctl hw.acpi.battery.state | awk '{ print $2 }'

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(8) and prints only the number which is at the second argument.

We are 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:

$ sysctl hw.acpi.battery.life | awk '{ print $2 }'

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

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"
            ;;
        6)
            OUTPUT="critical"
            ;;
        *)
            OUTPUT="ERR"
            ;;
    esac

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

Getting Network Information

The network data can be retrieved by ifconfig(8). 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.

info_network_status() {
    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”.

Getting Audio Information

Volume information is managed by mixer(8) inside FreeBSD.

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.

Getting RAM Information

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:

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(1).

— 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(1). The third pipe gets only the number expressed in MB without the “MB” wording.

— The variable PRCNTUSED is the basic formula to compare both numbers and determine the percentage used. We are using awk(1) to achieve it and in order to pass variables to awk(1) we need to tell it before the calculations. -v x=$y is the way to define a variable inside awk(1). 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(1) to leave only two decimals to our percentage result.

Getting CPU load Information

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.

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

Getting Disk space Information

Using the command df(1) 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:

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

Adding the flag -H after the command df(1) 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 then 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.

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.

Improving performance

After fetching all the required info from the system now it's time to give some personality to the panel bar and some performance under the hood.

We want to pass the data using a pipe and not a file since we don't want our bar to affect our computer's performance.

To be exact we are going to be using a named pipe (FIFO) which is a special file similar to a pipe except that it is accessed as part of the filesystem. It does nothing until some process reads and writes to it. It doesn't take any space on the hard disk, it doesn't use system memory and it can be opened by multiple processes for reading or writing.

In our bar script let's create one. Define a variable for our named pipe:

PANEL_FIFO=/tmp/panel-fifo

Now let's verify that we don't have any file named panel-fifo before starting our script and if we do, we are removing it and creating it from zero:

[ -e "$PANEL_FIFO" ] && rm "$PANEL_FIFO"
mkfifo "$PANEL_FIFO"

Once we have our named pipe created we can pass all our data-harvesting functions to it using redirection.

info_cpu > "$PANEL_FIFO" &
info_ram > "$PANEL_FIFO" &
info_drive_space > "$PANEL_FIFO" &
info_time_date > "$PANEL_FIFO" &
info_network_status > "$PANEL_FIFO" &
info_volume > "$PANEL_FIFO" &
info_battery > "$PANEL_FIFO" &

Our named pipe needs to write the data somewhere. Let's encapsulate our final while loop into a function so we can get our panel-fifo data through it:

panel_bar() {
    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
}

Now we can get our code executed adding the following line at the bottom:

panel_bar < "$PANEL_FIFO" | lemonbar &

If you try to execute your script after saving it you should see it running as before.

$ .scripts/status_panel.sh

Clean exit

Maybe you notice that after killing the terminal emulator instance that executed the script, it's still running. When we create temporary files in a script they are cleaned when the script exits successfully. But if we interrupt the script (like killing a terminal emulator instance running it) it may happen that the temp file isn't cleaned.

We can fix it using a really cool shell utility named trap. It captures interruptions in the script and cleans things up when interruption signals are caught.

trap 'trap - TERM; kill 0' INT TERM QUIT EXIT

This way we're good to go with our panel info.

Ricing Lemonbar

An important part of ricing is giving personal style to our work. Lemonbar can be filled with custom fonts and colors.

In terms of fonts, the standard Lemonbar version only supports bitmap fonts so if you want to use TTF fonts you need to get a modified port and build it. There's nothing wrong with bitmap fonts except from the scaling side. They render fast and they are pretty.

Before adding custom fonts to our script we need to get the system ready to read our bitmap fonts. In this example we're going to use Tamsyn from Scott Fial. Download the .tar.gz file from the link and extract it on your ~/.fonts directory.

After doing so we have to update the font cache and tell the X server bout our new font:

$ fc-cache -vf
$ xset +fp ~/.fonts
$ xset fp rehash

Chances are that after typing the xset command you're given an error instead of a successful operation. In this case let's make sure we have a font index in our directory.

$ cd .fonts
$ mkfontscale
$ mkfontdir

And re-type the xset instructions again.

Bitmap fonts aren't declared the same way as xft fonts. To make our life easier there's a tool named xfontsel() that can help us getting the value for the desired font (you may need to install it).

In our script status_panel let's create a variable for our custom font:

PANEL_FONT_0="-misc-tamsyn-medium-r-normal-*-20-145-100-100-c-100-iso8859-1"

The long name has been taken from the xfontsel() tool.

Another cool things in ricing are icons. There are both xft and bitmap icon fonts. Siji is a bitmap icon font that suits well into lemonbar.

There is a tricky part with icon fonts: you need to print an unicode glyph in order to get the icon visible. sh doesn't interpret raw unicode sequences so we have to use another shell like bash for this task.

Using xfd we can select siji's icons and look for the unicode that represents them.

$ xfd -fa wuncon\ siji

After knowing which icon unicodes we want, we have to pass them out through the script.

# this should print a bold square clock icon:
printf "\ue017"

The same way we added the previous font to our status_panel script, let's add the icon one:

PANEL_FONT_1="-wuncon-siji-medium-r-normal-*-10-100-75-75-c-80-iso10646-1"

All the custom parts that we may add to our status panel are given to lemonbar at the execution time. Add the font at the last line we have like this:

panel_bar < "$PANEL_FIFO" | lemonbar -f $PANEL_FONT_0 -f $PANEL_FONT_1 &

Now you should see your custom font when launching the script again.

Summing up

Adding the instruction to launch the script at our bspwmrc file or at our .xinitrc file will make it run from the moment we start an X server.

Further exploring is to add colors depending each state and icons using icon bitmap fonts. Check this repository by Tecate to look at a good bitmap fonts collection.

This is the final result of our script:

# ~/.scripts/status_panel

#!/bin/sh
PANEL_FONT_0="-misc-tamsyn-medium-r-normal-*-20-145-100-100-c-100-iso8859-1"
PANEL_FIFO=/tmp/panel-fifo

trap 'trap - TERM; kill 0' INT TERM QUIT EXIT

[ -e "$PANEL_FIFO" ] && rm "$PANEL_FIFO"
mkfifo "$PANEL_FIFO"

# all the functions described above are here !

info_cpu > "$PANEL_FIFO" &
info_ram > "$PANEL_FIFO" &
info_drive_space > "$PANEL_FIFO" &
info_time_date > "$PANEL_FIFO" &
info_network_status > "$PANEL_FIFO" &
info_volume > "$PANEL_FIFO" &
info_battery > "$PANEL_FIFO" &

panel_bar() {
    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
}

panel_bar < "$PANEL_FIFO" | lemonbar -f $PANEL_FONT_0 -f $PANEL_FONT_1 &

wait