Preface

Goal: Manage Herbstclient process trigerred by idle event

Focusing in "herbstclient --idle". 

HerbstluftWM: Tag Status

This is the next Part, of the previous Tutorial. This tutorial cover Lemonbar. 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.

The PipeHandler Source File:

Let’s have a look at pipehandler.hs in github.


HerbstluftWM Idle Event 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: [ Idle Event 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 ]


Statusbar Screenshot

Dzen2

Statusbar: Dzen2 Screenshot

Lemonbar

Statusbar: Lemonbar Screenshot


Without Idle event

Let’s have a look at our main panel.hs in github. At the end of the script, we finally call lemonbar with detachLemon function.

main = do
    ...

    -- remove all lemonbar instance
    system "pkill lemonbar"

    -- run process in the background
    detachLemon monitor lemonParameters

View Source File:

Run Lemon, Run !

This detachLemon function. is just a function that enable the lemonbar running process to be detached, using forkProcess().

detachLemon :: Int -> [String] -> IO ProcessID
detachLemon monitor parameters = forkProcess 
    $ runLemon monitor parameters

The real function is runLemon. You must be familiar with this createProcess.

runLemon :: Int -> [String] -> IO ()
runLemon monitor parameters = do
    let command_out = "lemonbar"

    (Just pipe_lemon_in, _, _, ph) <- 
        createProcess (proc command_out (parameters ++ ["-p"])) 
        { std_in = CreatePipe }

    contentInit monitor pipe_lemon_in    
    hClose pipe_lemon_in

Note: that we want to ignore idle event for a while. And append the -p for a while, to make the statusbar persistent.

Statusbar Initialization

Here we have the contentInit. It is just an initialization of global variable. We are going to have some loop later in different function, to do the real works.

contentInit :: Int -> Handle -> IO ()
contentInit monitor pipe_lemon_in = do
    setTagValue monitor 
    setWindowtitle ""
    
    text <- getStatusbarText monitor

    hPutStrLn pipe_lemon_in text
    hFlush pipe_lemon_in

Now is time to try the panel, on your terminal. Note: that we already reach this stage in our previous article. These two functions, setTagValue and setWindowtitle, have already been discussed.

View Source File:

Simple version. No idle event. Only statusbar initialization.


With Idle event

Consider this contentWalk call, after contentInit call, inside the runLemon.

runLemon :: Int -> [String] -> IO ()
runLemon monitor parameters = do
    let command_out = "lemonbar"

    (Just pipe_lemon_in, _, _, ph) <- 
        createProcess (proc command_out parameters) 
        { std_in = CreatePipe }

    contentInit monitor pipe_lemon_in
    contentWalk monitor pipe_lemon_in  -- loop for each event
    
    hClose pipe_lemon_in

Wrapping Idle Event into Code

contentWalk is the heart of this script. We have to capture every event, and process the event in event handler.

Walk step by step, Process event by event

After the event handler, we will get the statusbar text, in the same way, we did in content_init.

contentWalk :: Int -> Handle -> IO ()
contentWalk monitor pipe_lemon_in = do
    let command_in = "herbstclient"

    (_, Just pipe_idle_out, _, ph) <- 
        createProcess (proc command_in ["--idle"]) 
        { std_out = CreatePipe }

    forever $ do
        -- wait for next event 
        event <- hGetLine pipe_idle_out 
        handleCommandEvent monitor event
 
        text <- getStatusbarText monitor

        hPutStrLn pipe_lemon_in text
        hFlush pipe_lemon_in

    hClose pipe_idle_out

The Event Handler

For each idle event, there are multicolumn string. The first string define the event origin.

HerbstluftWM: Tag Status

The origin is either reload, or quit_panel, tag_changed, or tag_flags, or tag_added, or tag_removed, or focus_changed, or window_title_changed. More complete event, can be read in herbstclient manual.

All we need is to pay attention to this two function. set_tag_value and set_windowtitle.

getColumnTitle :: [String] -> String
getColumnTitle column
  | length(column) > 2 = column !! 2
  | otherwise          = ""

handleCommandEvent :: Int -> String -> IO ()
handleCommandEvent monitor event
  | origin == "reload"      = do system("pkill lemonbar"); return ()
  | origin == "quit_panel"  = do exitSuccess; return ()
  | elem origin tagCmds     = do setTagValue monitor
  | elem origin titleCmds   = do setWindowtitle $ getColumnTitle column
  where
    tagCmds   = ["tag_changed", "tag_flags", "tag_added", "tag_removed"]
    titleCmds = ["window_title_changed", "focus_changed"]

    -- find out event origin
    column = splitOn "\t" event
    origin = column !! 0

Actually that’s all we need to have a functional lemonbar. This is the minimum version.

View Source File:

With idle event. The heart of the script.


Lemonbar Clickable Areas

This is specific issue for lemonbar, that we don’t have in dzen2.

Consider have a look at output.hs.

    -- clickable tags
    textName  = "%{A:herbstclient focus_monitor \"" 
        ++ show(monitor) ++ "\" && " ++ "herbstclient use \"" 
        ++ tagIndex ++ "\":} " ++ tagName ++ " %{A} "

Issue: Lemonbar put the output on terminal instead of executing the command.

HerbstluftWM: Tag Status

Consider going back to pipehandler.hs.

We need to pipe the lemonbar output to shell. It means Lemonbar read input and write output at the same time. createProcess does good with bidirectional pipe.

runLemon :: Int -> [String] -> IO ()
runLemon monitor parameters = do
    let command_out = "lemonbar"

    (Just pipe_lemon_in, Just pipe_lemon_out, _, ph) <- 
        createProcess (proc command_out parameters) 
        { std_in = CreatePipe, std_out = CreatePipe }

    (_, _, _, ph) <- 
        createProcess (proc "sh" []) 
        { std_in = UseHandle pipe_lemon_out }

    contentInit monitor pipe_lemon_in
    contentWalk monitor pipe_lemon_in  -- loop for each event
    
    hClose pipe_lemon_in
    hClose pipe_lemon_out

How does it work ?

UseHandle take care of this.
        { std_in = UseHandle pipe_out }

View Source File:

Piping lemonbar output to shell, implementing lemonbar clickable area.


Interval Based Event

We can put custom event other than idle event in statusbar panel. This event, such as date event, called based on time interval in second.

It is a little bit tricky, because we have to make, a combined event that consist of, idle event (asynchronous) and interval event (synchronous). Merging two different paralel process into one.

This is an overview of what we want to achieve.

HerbstluftWM: Custom Event

In real code later, we do not need the timestamp. interval string is enough to trigger interval event.

View Testbed Source File:

Before merging combined event into main code, consider this test in an isolated fashion.


Combined Event

Preparing The View

This is what it looks like, an overview of what we want to achieve.

Statusbar: Event Screenshot

Consider make a progress in output.hs.

module MyOutput ( ..., setDatetime, ...) where

segmentDatetime :: IORef String
segmentDatetime = unsafePerformIO $ newIORef ""    -- empty string
 
wFormatTime :: FormatTime t => t -> String -> String
wFormatTime myUtcTime myTimeFormat = formatTime 
    Data.Time.Format.defaultTimeLocale myTimeFormat myUtcTime

getStatusbarText :: Int -> IO String
getStatusbarText monitor = do
    tags <- readIORef tagsStatus
    
    let tagText = "%{l}" ++ (join $ map (outputByTag monitor) tags)
    timeText  <- ("%{c}" ++) <$> outputByDatetime
    titleText <- ("%{r}" ++) <$> outputByTitle

    let text = tagText ++ timeText ++ titleText
    return text

outputByDatetime :: IO String
outputByDatetime = do
    segment <- readIORef segmentDatetime
    return segment

formatDatetime :: ZonedTime -> String
formatDatetime now = dateText ++ "  " ++ timeText
  where
    ...

setDatetime :: IO ()
setDatetime = do
    now <- getZonedTime     
    writeIORef segmentDatetime $ formatDatetime now

And a few enhancement in MyPipeHandler.hs.

wSleep :: Int -> IO ()
wSleep mySecond = threadDelay (1000000 * mySecond)

handleCommandEvent :: Int -> String -> IO ()
handleCommandEvent monitor event
  ...
  | origin == "interval"    = do setDatetime
  where
    ...

contentInit :: Int -> Handle -> IO ()
contentInit monitor pipe_lemon_in = do
    ... 
    setWindowtitle ""
    setDatetime

    ...

Expanding The Event Controller

All we need to do is to split out content_walk into

  • content_walk: combined event, with the help of cat process.

  • content_event_idle: HerbstluftWM idle event. Forked, as background processing.

  • content_event_interval : Custom date time event. Forked, as background processing.

contentEventIdle :: Handle -> IO ()
contentEventIdle pipe_cat_in = do
    let command_in = "herbstclient"

    (_, Just pipe_idle_out, _, ph) <- 
        createProcess (proc command_in ["--idle"]) 
        { std_out = CreatePipe }

    forever $ do
        -- wait for next event 
        event <- hGetLine pipe_idle_out

        hPutStrLn pipe_cat_in event
        hFlush pipe_cat_in

    hClose pipe_idle_out
contentEventInterval :: Handle -> IO ()
contentEventInterval pipe_cat_in = forever $ do
     hPutStrLn pipe_cat_in "interval"
     hFlush pipe_cat_in

     wSleep 1
contentWalk :: Int -> Handle -> IO ()
contentWalk monitor pipe_lemon_in = do
    (Just pipe_cat_in, Just pipe_cat_out, _, ph) <- 
        createProcess (proc "cat" []) 
        { std_in = CreatePipe, std_out = CreatePipe }

    forkProcess $ contentEventIdle(pipe_cat_in)
    forkProcess $ contentEventInterval(pipe_cat_in)
    
    forever $ do
        -- wait for next event 
        event <- hGetLine pipe_cat_out
        handleCommandEvent monitor event
 
        text <- getStatusbarText monitor

        hPutStrLn pipe_lemon_in text
        hFlush pipe_lemon_in

    hClose pipe_cat_out
    hClose pipe_cat_in

This above is the most complex part. We are almost done.

View Source File:

Combined event consist of both, synchronous interval event and asynchronous idle event.


Dual Bar

The idea of this article comes from the fact that herbsclient --idle is asynchronous event. If you need another bar, just simply use Conky instead.

  • Dzen2: HerbstluftWM: Dzen2 Conky

  • Lemonbar: HerbstluftWM: Lemonbar Conky

We only need one function to do this in pipehandler.pm.

detachLemonConky :: [String] -> IO ()
detachLemonConky parameters = do
    -- Source directory is irrelevant in Haskell
    -- but we'll do it anyway for the sake of learning
    dirName <- getCurrentDirectory
    let conkyFileName = dirName ++ "/../conky" ++ "/conky.lua" 

    (_, Just pipeout, _, _) <- 
        createProcess (proc "conky" ["-c", conkyFileName])
        { std_out = CreatePipe } 

    (_, _, _, ph)  <- 
        createProcess (proc "lemonbar" parameters) 
        { std_in = UseHandle pipeout }
      
    hClose pipeout

And execute the function main script in panel.pl.

-- This is a modularized config for herbstluftwm tags in lemonbar

import System.Environment
import System.Process

import MyHelper
import MyPipeHandler

-- initialize

panelHeight = 24

-- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- -----
-- main

main = do
    args <- getArgs
    let monitor = getMonitor args

    geometry <- getGeometry monitor

    system "pkill lemonbar"
    system $ "herbstclient pad " ++ show(monitor) ++ " "
        ++ show(panelHeight) ++ " 0 " ++ show(panelHeight) ++ " 0"

    -- run process in the background

    let paramsTop = getParamsTop panelHeight geometry
    detachLemon monitor paramsTop

    let paramsBottom = getParamsBottom panelHeight geometry
    detachLemonConky paramsBottom

    -- end of IO
    return ()

View Source File:

Dual Bar, detach_lemon_conky function.


Avoid Zombie Apocalypse

Zombie are scary, and fork does have a tendecy to become a zombie. Application that utilize several forks should be aware of this threat. The reason why I use fork instead of thread is, because the original herbstluftwm configuration coming from bash, and this bash script is using fork.

However, you can use this short script to reduce zombie population. It won’t kill all zombie, but works for most case. You might still need htop, and kill -9 manually.

killZombie :: IO ()
killZombie = do
    system "pkill -x dzen2"
    system "pkill -x lemonbar"
    system "pkill -x cat"
    system "pkill conky"
    system "pkill herbstclient"
    
    return ()

Putting Them All Together

I also created compact for version, for use with main HerbstluftWM configuration, in ~/.config/herbstluftwm/ directory. After reunification, they are not very long scripts after all.


Desktop Screenshot

Fullscreen, Dual Panel, Zero Gap.

HerbstluftWM: Screenshot Dual Panel


Enjoy the statusbar ! Enjoy the window manager !