Preface

Goal: Separate Main Flow, Code, and Data.

So anyone can focus to alter special customization in Main Script, without changing the whole stuff.

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.


Modularized HerbstluftWM 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: [ Modularized Overview ] [ BASH ] [ Perl ] [ Python ] [ Ruby ] [ PHP ] [ Lua ] [ Haskell ]

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


Directory Structure

Directory Structure has been explained in preface. This figure will explain how it looks in Lua script directory.

HerbstluftWM: Directory Structure


Modularizing in Lua

Lua is simple, except for importing module using relative path. This require a workaround with package.path.

Declare a module

No need to explicitly define what to export.

local _M = {}

function _M.hc(arguments)
    os.execute("herbstclient " .. arguments)
end

return _M

Call a module

Note the dot ..

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

local helper  = require(".helper")

System Calls

Here we wrap herbstclient system call in a function named hc.

helper.lua

function _M.hc(arguments)
    os.execute("herbstclient " .. arguments)
end

autostart.lua

-- Read the manual in $ man herbstluftwm
helper.hc('emit_hook reload')

-- gap counter
os.execute("echo 35 > /tmp/herbstluftwm-gap")

Array: Tag Names and Keys

Is it just me? Or didn’t I do googling hard enough ? I cannot find a way to define array by range in just one line.

config.lua

_M.tag_names = {}; _M.tag_keys = {} -- new array

for i = 1,9 do _M.tag_names[i-1]=i end
for i = 1,9 do _M.tag_keys[i-1]=i  end

_M.tag_keys[9] = 0

HerbstluftWM: Tag Status


Hash: Color Schemes

Using key-value pairs, a simple data structure.

gmc.lua

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

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

autostart.lua

-- background before wallpaper
os.execute("xsetroot -solid '" .. gmc.color["blue500"] .. "'")

View Source File:

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


Hash: Config

The Hash in Config is very similar with the colors above. Except that it has string concatenation all over the place.

config.lua

-- Modifier variables
s = 'Shift';
c = 'Control';
m = 'Mod4';
a = 'Mod1';

_M.keybinds = {
  -- session
    [m .. '-' .. s .. '-q'] = 'quit',
    [m .. '-' .. s .. '-r'] = 'reload',
    [m .. '-' .. s .. '-c'] = 'close',
}

This config will be utilized in main script as shown in the following code.

autostart.lua

helper.do_config('keybind',   config.keybinds)
helper.do_config('keybind',   config.tagskeybinds)
helper.do_config('mousebind', config.mousebinds)
helper.do_config('attr',      config.attributes)
helper.do_config('set',       config.sets)
helper.do_config('rule',      config.rules)

View Source File:

Similar Code: [ BASH Config ] [ Perl Config ] [ Python Config ] [ Ruby Config ] [ PHP Config ] [ Lua Config ] [ Haskell Config ]


Processing The Hash Config

This is the heart of this script.

This do-config function has two arguments, the herbstclient command i.e “keybind”, and hash from config. I can see how simple and clean, Lua is.

helper.lua

function _M.do_config(command, hash)
    -- loop over hash
    for key, value in pairs(hash) do 
        _M.hc(command .. ' ' .. key .. ' ' .. value)

        -- uncomment to debug in terminal
        -- print(command .. ' ' .. key .. ' ' .. value)
    end
end

Debug Herbstclient Command

I do not remove line where I do debug when I made this script, so anyone can use it later, avoid examining blindly. Sometimes strange things happen. Just uncomment this line to see what happened.

        print(command .. ' ' .. key .. ' ' .. value)

You can see the debugging result in figure below.

HerbstluftWM: Debug Command

View Source File:

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


Setting the Tags

Nothing special here, Ruby read all exported variable from modules. And I define local tag_names to avoid long namespace.

helper.lua

local config  = require(".config")

function _M.set_tags_with_name()
    local tag_names = config.tag_names
    local tag_keys = config.tag_keys    
    
    _M.hc("rename default '" .. tag_names[0] .. "' 2>/dev/null || true")
    
    for index, value in pairs(tag_names) do 
        _M.hc("add '" .. value .. "'");
        
        local key = tag_keys[index]
        if (not (key == nil or key == '') ) then
            _M.hc("keybind Mod4-"..key.." use_index '"..index.."'")
            _M.hc("keybind Mod4-Shift-"..key.." move_index '"..index.."'")
        end
    end
end

Launch the Panel

Two more functions left, it is do_panel and startup_run.

This two is longer in Lua, compared to another script. helper.lua

function _M.do_panel() 
    local dirname  = debug.getinfo(1).source:match("@?(.*/)")   
    local panel   = dirname .. "panel-lemonbar.lua"
    if (not file_exists(panel)) then
        panel = "/etc/xdg/herbstluftwm/panel.sh"
    end

    command = 'herbstclient list_monitors | cut -d: -f1'
    local handle = io.popen(command)
    local result = handle:read("*a")
    handle:close()
    
    local raw = trim1(result) 
    local monitors = lines(result)
    
    for i, monitor in pairs(monitors) do 
        if (not (monitor == nil or monitor == '') ) then
            -- start it on each monitor
            os.execute(panel .. " " .. monitor .." > /dev/null &")
        end
    end
end

Specific Lua Issue

Is it just me? Or didn’t I do googling hard enough ? I cannot find a way to make the script shorter. It is almost three times longer than the BASH counterpart.


Run Baby Run

This is the last part. It is intended to be modified. Everyone has their own personal preferences.

startup.lua

function _M.run()
    -- redirect stderror to stdout, then capture the result
    command = 'new_attr bool my_not_first_autostart'
    local handle = io.popen('herbstclient ' .. command .. ' 2>&1')
    local result = handle:read('*a')
    local exitcode = handle:close()
    
    if ((result == nil or result == '')) then
     -- non windowed app
        os.execute('compton &')
        os.execute('dunst &')
        os.execute('parcellite &')
        os.execute('nitrogen --restore &')
        os.execute('mpd &')

     -- windowed app
        os.execute('xfce4-terminal &')
        os.execute('sleep 1 && firefox &')
        os.execute('sleep 2 && geany &')
        os.execute('sleep 2 && thunar &')
    end
end

Specific Lua Issue

Is it just me? Or didn’t I do googling hard enough ? Instead of exitcode, I’m using standard error to determine whether it should be launch or not.

View Source File:

Similar Code: [ BASH Startup ] [ Perl Startup ] [ Python Startup ] [ Ruby Startup ] [ PHP Startup ] [ Lua Startup ] [ Haskell Startup ]


Putting It All Together.

The last part is going to main script and putting it all back together.

Now the flow is clear

Header Part: autostart.lua

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

local gmc     = require ".gmc"
local helper  = require(".helper")
local config  = require(".config")
local startup = require(".startup")

Procedural Part: autostart.lua

-- background before wallpaper
os.execute("xsetroot -solid '" .. gmc.color["blue500"] .. "'")

-- Read the manual in $ man herbstluftwm
helper.hc('emit_hook reload')

-- gap counter
os.execute("echo 35 > /tmp/herbstluftwm-gap")

-- do not repaint until unlock
helper.hc("lock")

-- standard
helper.hc('keyunbind --all')
helper.hc('mouseunbind --all')
helper.hc('unrule -F')

helper.set_tags_with_name()

-- do hash config
helper.do_config('keybind',   config.keybinds)
helper.do_config('keybind',   config.tagskeybinds)
helper.do_config('mousebind', config.mousebinds)
helper.do_config('attr',      config.attributes)
helper.do_config('set',       config.sets)
helper.do_config('rule',      config.rules)

-- unlock, just to be sure
helper.hc("unlock")

-- launch statusbar panel (e.g. dzen2 or lemonbar)
helper.do_panel()

-- load on startup
startup.run()

View Source File:

Similar Code: [ BASH autostart ] [ Perl autostart ] [ Python autostart ] [ Ruby autostart ] [ PHP autostart ] [ Lua autostart ] [ Haskell autostart ]


Coming up Next

After the Window Manager, comes the Panel.


Happy Configuring.