Preface

Goal: Show the Herbstclient Tag.

Focusing in "herbstclient tag_status". 

HerbstluftWM: Tag Status

This tutorial cover Lemonbar, and in order to use Dzen2, any reader could use the source code in github.


Reading

Before you jump off to scripting, you might desire to read this overview.

All The Source Code:

Impatient coder like me, like to open many tab on browser.


HerbstluftWM Tag Status in Many Languages

This article is one part of a collection. All integrated, on related to another. So we can compare each other quickly.

Tutorial/ Guidance/ Article: [ Tag Status Overview ] [ BASH ] [ Perl ] [ Python ] [ Ruby ] [ PHP ] [ Lua ] [ Haskell ]

Dzen2 Source Code Directory: [ BASH ] [ Perl ] [ Python ] [ Ruby ] [ PHP ] [ Lua ] [ Haskell ]

Lemonbar Source Code Directory: [ BASH ] [ Perl ] [ Python ] [ Ruby ] [ PHP ] [ Lua ] [ Haskell ]


Screenshot

Since window manager is out of topic in this tutorial, I present only panel HerbstluftWM screenshot.

Dzen2

Statusbar: Dzen2 Screenshot

Lemonbar

Statusbar: Lemonbar Screenshot


Directory Structure

Directory Structure has been explained in preface. For both Dzen2 and Lemonbar, the structure are the same. This figure will explain how it looks in Lua script directory.

Statusbar: Directory Structure

Special customization can be done in output script, without changing the whole stuff.


Common Module

Lua is an embedding language. It is suppose to be light. No wonder it lacks of standard function for daily coding, such as split, trim, sleep, that we are going to use in this project.

I don’t like to grumble. I’d better collect from some resurces in the internet.

common.lua

local _M = {}

function _M.sleep (n)
    local t = os.clock()
    while os.clock() - t <= n do
        -- nothing
    end
end

function _M.split(inputstr, sep)
        if sep == nil then
                sep = "%s"
        end
        local t={} ; i=1 -- non zero based
        for str in string.gmatch(inputstr, "([^"..sep.."]+)") do
                t[i] = str
                i = i + 1
        end
        return t
end

function _M.trim1(s)
  return (s:gsub("^%s*(.-)%s*$", "%1"))
end

function _M.has_value (tab, val)
    for index, value in ipairs(tab) do
        if value == val then
            return true
        end
    end

    return false
end

View Source File:


Get Geometry

Let’s have a look at helper.lua in github.

View Source File:

Similar Code (Dzen2): [ BASH Helper ] [ Perl Helper ] [ Python Helper ] [ Ruby Helper ] [ PHP Helper ] [ Lua Helper ] [ Haskell Helper ]

Similar Code (Lemonbar): [ BASH Helper ] [ Perl Helper ] [ Python Helper ] [ Ruby Helper ] [ PHP Helper ] [ Lua Helper ] [ Haskell Helper ]

Get Script Argument

The original herbstluftwm panel example, contain statusbar for each monitor. The default is using monitor 0, although you can use other monitor as well.

$ ./panel.lua 0

I do not implement statusbar in multi monitor since I only have my notebook. But I’ll pass the argument anyway for learning purpose. Here it is our code in Lua.

helper.lua

local _M = {}

function _M.get_monitor(arguments)
    -- no ternary operator, using expression
    return (#arguments > 0) and tonumber(arguments[1]) or 0
end

Calling external module in Lua using relative directory need special tricks.

local dirname  = debug.getinfo(1).source:match("@?(.*/)")
package.path   = package.path .. ';' .. dirname .. '?.lua;'

local helper      = require('.helper')

Now in main code we can call

local monitor = helper.get_monitor(arg)
print(monitor)

This will display 0 or else such as 1, depend on the script argument given.

0

Get Monitor Geometry

HerbstluftWM give this little tools to manage monitor geometry by getting monitor rectangle.

$ herbstclient monitor_rect

This will show something similar to this.

0 0 1280 800

HerbstluftWM: Monitor Rectangle

Consider wrap the code into function. And get an array as function return.

helper.lua

function _M.get_geometry(monitor)
    local command = 'herbstclient monitor_rect ' .. monitor
    local handle = io.popen(command)
    local result = handle:read("*a")
    handle:close()

    if (result == nil or result == '') then
        print('Invalid monitor ' .. monitors)
        os.exit()
    end      
        
    local raw = common.trim1(result)  
    local geometry = common.split(raw, ' ')
    
    return geometry
end

Consider call this function from script later. To print array in Lua, we just have to wrap it in table.concat(geometry, ' '). Just remember that Lua array is not zero based.

local monitor  = helper.get_monitor(arg)
local geometry = helper.get_geometry(monitor)
print(table.concat(geometry, ' '))

This will produce

0 0 1280 800

Get Panel Geometry

The Panel geometry is completely depend on the user flavor and taste. You can put it, on top, or bottom, or hanging somewhere. You can create gap on both left and right.

Consider this example: helper.lua

function _M.get_bottom_panel_geometry(height, geometry)
    -- geometry has the format X Y W H
    return tonumber(geometry[1]) + 24, tonumber(geometry[4]) - height, 
           tonumber(geometry[3]) - 48, height
end

We are going to use this X Y W H, to get lemonbar parameter.

local panel_height = 24
local monitor  = helper.get_monitor(arg)
local geometry = helper.get_geometry(monitor)
local xpos, ypos, width, height = helper.get_bottom_panel_geometry(
      panel_height, geometry)

print('Lemonbar geometry: ' 
    .. tostring(width) .. 'x' .. tostring(height) .. '+'
    .. tostring(xpos)  .. '+' .. tostring(ypos))

This will show something similar to this result, depend on your monitor size.

Lemonbar geometry: 1280x24+24+776

Get Lemonbar Parameters

We almost done. This is the last step. We wrap it all inside this function below.

helper.lua

function _M.get_lemon_parameters(monitor, panel_height)
    -- calculate geometry
    local geometry = _M.get_geometry(monitor)
    local xpos, ypos, width, height = _M.get_bottom_panel_geometry(
        panel_height, geometry)

    -- geometry: -g widthxheight+x+y
    local geom_res = tostring(width) .. 'x' .. tostring(height)
           .. '+' .. tostring(xpos)  .. '+' .. tostring(ypos)

    -- color, with transparency    
    local bgcolor = "'#aa000000'"
    local fgcolor = "'#ffffff'"

    -- XFT: require lemonbar_xft_git 
    local font_takaop  = "takaopgothic-9"
    local font_bottom  = "monospace-9"
    local font_symbol  = "PowerlineSymbols-11"
    local font_awesome = "FontAwesome-9"
  
    local parameters = ""
        .. " -g "..geom_res.." -u 2"
        .. " -B "..bgcolor.." -F "..fgcolor
        .. " -f "..font_takaop
        .. " -f "..font_awesome
        .. " -f "..font_symbol
        
    return parameters
end

Testing The Parameters

Consider this code 01-testparams.lua. The script call the above function to get lemon parameters.

#!/usr/bin/lua

local dirname  = debug.getinfo(1).source:match("@?(.*/)")
package.path   = package.path .. ';' .. dirname .. '?.lua;'
  
local helper      = require('.helper')

-- initialize
local panel_height = 24
local monitor = helper.get_monitor(arg)

local lemon_parameters = helper.get_lemon_parameters(monitor, panel_height)
print(lemon_parameters)

This will produce output something similar to this result

-g 1280x24+0+776 -u 2 -B '#aa000000' -F '#ffffff' 
-f takaopgothic-9 -f FontAwesome-9 -f PowerlineSymbols-11

Or in Dzen2 version:

-x 0 -y 0 -w 1280 -h 24 -ta l 
-bg '#000000' -fg '#ffffff' -title-name dzentop 
-fn '-*-takaopgothic-medium-*-*-*-12-*-*-*-*-*-*-*'

View Source File:


Adjusting the Desktop

Since we want to use panel, we have to adjust the desktop gap, giving space at the top and bottom.

$ herbstclient pad 0 24 0 24 0

For more information, do $ man herbsluftclient, and type \pad to search what it means.

In script, it looks like this below.

os.execute('herbstclient pad ' .. monitor .. ' ' 
    .. panel_height .. ' 0 ' .. panel_height .. ' 0')

Color Schemes

Using a simple data structure key-value pairs, we have access to google material color for use with dzen2 or lemonbar. Having a nice pallete to work with, makes our panel more fun.

gmc.lua

_M.color = {
    ['white'] = '#ffffff',
    ['black'] = '#000000',

    ['grey50']  = '#fafafa',
    ['grey100'] = '#f5f5f5'
}

View Source File:

Similar Code (Dzen2): [ BASH Color ] [ Perl Color ] [ Python Color ] [ Ruby Color ] [ PHP Color ] [ Lua Color ] [ Haskell Color ]

Similar Code (Lemon): [ BASH Color ] [ Perl Color ] [ Python Color ] [ Ruby Color ] [ PHP Color ] [ Lua Color ] [ Haskell Color ]


Preparing Output

Let’s have a look at output.lua in github.

View Source File:

Similar Code (Dzen2): [ BASH Output ] [ Perl Output ] [ Python Output ] [ Ruby Output ] [ PHP Output ] [ Lua Output ] [ Haskell Output ]

Similar Code (Lemonbar): [ BASH Output ] [ Perl Output ] [ Python Output ] [ Ruby Output ] [ PHP Output ] [ Lua Output ] [ Haskell Output ]


Global Variable and Constant

Officialy there is a no way to define constant in Lua. Lua does not differ between these two.

Mutable State: Segment Variable

The different between interval based and event based is that, with interval based all panel segment are recalculated, while with event based only recalculate the trigerred segment.

In this case, we only have two segment in panel.

  • Tag

  • Title

output.lua In script, we initialize the variable as below

_M.segment_windowtitle = '' -- empty string
_M.tags_status = {}         -- empty table

Each segment buffered. And will be called while rendering the panel.

Global Constant: Tag Name

Assuming that herbstclient tag status only consist of nine number element.

$ herbstclient tag_status
	#1	:2	:3	:4	:5	.6	.7	.8	.9	

We can manage custom tag names, consist of nine string element. We can also freely using unicode string instead of plain one.

output.lua

_M.tag_shows = {'一 ichi', '二 ni', '三 san', '四 shi', 
  '五 go', '六 roku', '七 shichi', '八 hachi', '九 kyū', '十 jū'}

Global Constant: Decoration

output.lua Decoration consist lemonbar formatting tag.

local gmc = require('.gmc')
local _M = {}

-- decoration
_M.separator = '%{B-}%{F' .. gmc.color['yellow500'] .. '}|%{B-}%{F-}'

-- Powerline Symbol
_M.right_hard_arrow = ""
_M.right_soft_arrow = ""
_M.left_hard_arrow  = ""
_M.left_soft_arrow  = ""

-- theme
_M.pre_icon    = '%{F' .. gmc.color['yellow500'] .. '}'
_M.post_icon   = '%{F-}'

Segment Variable

As response to herbstclient event idle, these two function set the state of segment variable.

output.lua

function _M.set_tag_value(monitor)
    local command = 'herbstclient tag_status ' .. monitor
    local handle = io.popen(command)
    local result = handle:read("*a")
    handle:close() 
        
    local raw = common.trim1(result)  
    _M.tags_status = common.split(raw, "\t")
end

This function above turn the tag status string into array of tags for later use.

output.lua

function _M.set_windowtitle(windowtitle)
    local icon = _M.pre_icon .. '' .. _M.post_icon

    if (windowtitle == nil) then windowtitle = '' end
    
    windowtitle = common.trim1(windowtitle)
      
    _M.segment_windowtitle = ' ' .. icon ..
        ' %{B-}%{F' .. gmc.color['grey700'] .. '} ' .. windowtitle
end

We will call these two functions later.


Decorating: Window Title

This is self explanatory. I put separator, just in case you want to add other segment. And then returning string as result.

output.lua

function _M.output_by_title()
    local text = _M.segment_windowtitle .. ' ' .. _M.separator .. '  '

    return text
end

Decorating: Tag Status

This transform each plain tag such as .2, to decorated tag names such as 二 ni. Note that it only process one tag. We process all tags in a loop in other function.

This has some parts:

  • Pre Text: Color setting for Main Text (Background, Foreground, Underline). Arrow before the text, only for active tag.

  • Main Text: Tag Name by number, each with their tag state #, +, ., |, !, and each tag has clickable area setting.

  • Post Text: Arrow after the text, only for active tag.

  • Color Reset: %{B-}, %{F-}, %{-u} (Background, Foreground, Underline).

output.lua

function _M.output_by_tag(monitor, tag_status)
    local tag_index  = string.sub(tag_status, 2, 2)
    local tag_mark   = string.sub(tag_status, 1, 1)
    local index      = tonumber(tag_index)-- not a zero based array
    local tag_name   = _M.tag_shows[index]

    -- ----- pre tag

    local text_pre = ''
    if tag_mark == '#' then
        text_pre = '%{B' .. gmc.color['blue500'] .. '}'
                .. '%{F' .. gmc.color['black'] .. '}'
                .. '%{U' .. gmc.color['white'] .. '}%{+u}' 
                .. _M.right_hard_arrow
                .. '%{B' .. gmc.color['blue500'] .. '}'
                .. '%{F' .. gmc.color['white'] .. '}'
                .. '%{U' .. gmc.color['white'] .. '}%{+u}'
    elseif tag_mark == '+' then
        text_pre = '%{B' .. gmc.color['yellow500'] .. '}'
                .. '%{F' .. gmc.color['grey400'] .. '}'
    elseif tag_mark == ':' then
        text_pre = '%{B-}%{F' .. gmc.color['white'] .. '}'
                .. '%{U' .. gmc.color['red500'] .. '}%{+u}'
    elseif tag_mark == '!' then
        text_pre = '%{B' .. gmc.color['red500'] .. '}'
                .. '%{F' .. gmc.color['white'] .. '}'
                .. '%{U' .. gmc.color['white'] .. '}%{+u}'
    else
        text_pre = '%{B-}%{F' .. gmc.color['grey600'] .. '}%{-u}'
    end

    -- ----- tag by number

    -- clickable tags
    local text_name = '%{A:herbstclient focus_monitor '
                   .. '"' .. monitor .. '" && '
                   .. 'herbstclient use "' .. tag_index .. '":}'
                   .. ' ' .. tag_name ..' %{A} '
    
    -- non clickable tags
    -- local text_name = ' ' .. tag_name .. ' '

    -- ----- post tag

    local text_post = ""
    if (tag_mark == '#') then
        text_post = '%{B-}' 
                 .. '%{F' .. gmc.color['blue500'] .. '}' 
                 .. '%{U' .. gmc.color['red500'] .. '}%{+u}' 
                 .. _M.right_hard_arrow
    end

    text_clear = '%{B-}%{F-}%{-u}'

     
    return text_pre .. text_name .. text_post .. text_clear
end

Combine The Segments

Now it is time to combine all segments to compose one panel. Lemonbar is using %{l} to align left segment, and %{r} to align right segment. All tags processed in a loop.

output.lua

function _M.get_statusbar_text(monitor)
    local text = ''
    
    -- draw tags, zero based
    text = text .. '%{l}'
    for index = 1, #(_M.tags_status) do
        text = text .. _M.output_by_tag(monitor, _M.tags_status[index])
    end
    
    -- draw window title 
    text = text .. '%{r}'
    text = text .. _M.output_by_title()
  
    return text
end

Testing The Output

Consider this code 02-testoutput.lua. The script using pipe as feed to lemonbar.

We append -p parameter to make the panel persistent.

#!/usr/bin/lua

local dirname  = debug.getinfo(1).source:match("@?(.*/)")
package.path   = package.path .. ';' .. dirname .. '?.lua;'
local helper      = require('.helper')

-- process handler
function test_lemon(monitor, parameters) 
    local output = require('.output')

    local command_out  = 'lemonbar ' .. parameters .. ' -p'
    local pipe_out = assert(io.popen(command_out, 'w'))
    
    -- initialize statusbar before loop
    output.set_tag_value(monitor)
    output.set_windowtitle('test')

    local text = output.get_statusbar_text(monitor)
    pipe_out:write(text .. "\n")
    pipe_out:flush()
        
    pipe_out:close()
end

-- initialize
local panel_height = 24
local monitor = helper.get_monitor(arg)
local lemon_parameters = helper.get_lemon_parameters(
    monitor, panel_height)

-- test
os.execute('herbstclient pad ' .. monitor .. ' ' 
    .. panel_height .. ' 0 ' .. panel_height .. ' 0')

test_lemon(monitor, lemon_parameters)

This will produce a panel on top.

Statusbar: Lemonbar Screenshot

The panel only contain the initialized version of the text. It does not really interact with the HerbstluftWM event.

You can also click the clickable area to see it’s result. It only show text, not executed yet.

herbstclient focus_monitor "0" && herbstclient use "2"
herbstclient focus_monitor "0" && herbstclient use "3"

View Source File:


Coming up Next

It is already a long tutorial. It is time to take a break for a while.

We are going to continue on next tutorial to cover interaction between the script process and HerbstluftWM idle event.


Enjoy the statusbar !