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.php 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.php in github. At the end of the script, we finally call lemonbar with detach_lemon function.

# remove all lemonbar instance
system('pkill lemonbar')

# run process in the background
detach_lemon(monitor, lemon_parameters)

View Source File:

Run Lemon, Run !

This detach_lemon function. is just a function that enable the lemonbar running process to be detached, using $pid = pcntl_fork();.

function detach_lemon($monitor, $parameters)
{ 
    $pid_lemon = pcntl_fork();
    
    switch($pid_lemon) {         
    case -1 : // fork errror         
        die('could not fork');
    case 0  : // we are the child
        run_lemon($monitor, $parameters); 
        break;
    default : // we are the parent             
        return $pid_lemon;
    }    
}

The real function is run_lemon. Note that we are using proc_open instead of popen, to enable bidirectional pipe later.

function run_lemon($monitor, $parameters) 
{ 
    $descriptorspec = array(
        0 => array('pipe', 'r'),  // stdin
        1 => array('pipe', 'w'),  // stdout
        2 => array('pipe', 'w',)  // stderr
    );
    
    $command_out = "lemonbar $parameters -p";
    $proc_lemon = proc_open($command_out, $descriptorspec, $pipe_lemon);
    
    content_init($monitor, $pipe_lemon[0]);
    pclose($pipe_lemon[0]);
}

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 content_init. It is just an initialization of global variable. We are going to have some loop later in different function, to do the real works.

function content_init($monitor, $pipe_lemon_stdin)
{
    set_tag_value($monitor);
    set_windowtitle('');
        
    $text = get_statusbar_text($monitor);
    fwrite($pipe_lemon_stdin, $text."\n");
    flush();
}

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

View Source File:

Simple version. No idle event. Only statusbar initialization.


With Idle event

Consider this content_walk call, after content_init call, inside the run_lemon.

function run_lemon($monitor, $parameters) 
{ 
    $descriptorspec = array(
        0 => array('pipe', 'r'),  // stdin
        1 => array('pipe', 'w'),  // stdout
        2 => array('pipe', 'w',)  // stderr
    );
    
    $command_out = "lemonbar $parameters";
    $proc_lemon = proc_open($command_out, $descriptorspec, $pipe_lemon);
    
    content_init($monitor, $pipe_lemon[0]);
    content_walk($monitor, $pipe_lemon[0]); // loop for each event

    pclose($pipe_lemon[0]);
}

Wrapping Idle Event into Code

content_walk 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. popen is sufficient for unidirectional pipe.

function content_walk($monitor, $pipe_lemon_stdin)
{       
    // start a pipe
    $command_in    = 'herbstclient --idle';
    $pipe_idle_in  = popen($command_in,  'r'); // handle
    
    while(!feof($pipe_idle_in)) {
        # read next event
        $event = trim(fgets($pipe_idle_in));
        handle_command_event($monitor, $event);
        
        $text = get_statusbar_text($monitor);
        fwrite($pipe_lemon_stdin, $text."\n");
        flush();
    }
    
    pclose($pipe_idle_in);
}

We do not need to use proc_open for unidirectional pipe.


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.

function handle_command_event($monitor, $event)
{
    // find out event origin
    $column = explode("\t", $event);
    $origin = $column[0];

    switch($origin) {
    case 'reload':
        system('pkill lemonbar');
        break;
    case 'quit_panel':
        exit(1);
    case 'tag_changed':
    case 'tag_flags':
    case 'tag_added':
    case 'tag_removed':
        set_tag_value($monitor);
        break;
    case 'window_title_changed':
    case 'focus_changed':
        $title = count($column) > 2 ? $column[2] : '';
        set_windowtitle($title);
    }
}

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.php.

    // clickable tags
    $text_name = "%{A:herbstclient focus_monitor \"${monitor}\" && " 
               . "herbstclient use \"${tag_index}\":} ${tag_name} %{A}";

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

HerbstluftWM: Tag Status

Consider going back to pipehandler.php.

We need to pipe the lemonbar output to shell. It means Lemonbar read input and write output at the same time.

function run_lemon($monitor, $parameters) 
{ 
    $descriptorspec = array(
        0 => array('pipe', 'r'),  // stdin
        1 => array('pipe', 'w'),  // stdout
        2 => array('pipe', 'w',)  // stderr
    );
    
    $command_out  = "lemonbar $parameters";
    $proc_lemon = proc_open($command_out, $descriptorspec, $pipe_lemon);
    $proc_sh    = proc_open('sh', $descriptorspec, $pipe_sh);
    
    $pid_content = pcntl_fork();
    
    switch($pid_content) {         
    case -1 : // fork errror         
        die('could not fork');
    case 0  : // we are the child
        content_init($monitor, $pipe_lemon[0]);
        content_walk($monitor, $pipe_lemon[0]); // loop for each event
        break;
    default : // we are the parent
        while(!feof($pipe_lemon[1])) {
            $buffer = fgets($pipe_lemon[1]);
            fwrite($pipe_sh[0], $buffer);
        }
        return $pid_content;
    } 

    pclose($pipe_lemon[0]);
}

How does it work ?

Forking solve this issue.

Seriously, we have to take care on where to put the loop, without interfering the original loop in content_walk.

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.php.

$segment_datetime    = ''; # empty string

function get_statusbar_text($monitor)
{
    ...

    # draw date time
    $text .= '%{c}';
    $text .= output_by_datetime();

    ...
}

function output_by_datetime()
{
    global $segment_datetime; 
    return $segment_datetime;
}

function set_datetime() {
    ...

    $segment_datetime = "$date_text  $time_text";
}

And a few enhancement in pipehandler.php.

function handle_command_event($monitor, $event)
{
    ...

    switch($origin) {
    ...
    case 'interval':
        set_datetime();
    }
}

function content_init($monitor, $pipe_lemon_stdin)
{   
    ...
    set_windowtitle('');
    set_datetime();

    ...
}

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.

function content_event_idle($pipe_cat_stdin) 
{
    $pid_idle = pcntl_fork();

    switch($pid_idle) {         
    case -1 : // fork errror         
        die('could not fork');
    case 0  : // we are the child
        // start a pipe
        $command_in    = 'herbstclient --idle';
        $pipe_idle_in  = popen($command_in,  'r'); // handle
    
        while(!feof($pipe_idle_in)) {
            # read next event
            $event = fgets($pipe_idle_in);
            fwrite($pipe_cat_stdin, $event);
            flush();
        }
    
        pclose($pipe_idle_in);

        break;
    default : // we are the parent
        // do nothing
        return $pid_idle;
    } 
}
function content_event_interval($pipe_cat_stdin) 
{
    date_default_timezone_set("Asia/Jakarta");
    $pid_interval = pcntl_fork();

    switch($pid_interval) {         
    case -1 : // fork errror         
        die('could not fork');
    case 0  : // we are the child
        do {
            fwrite($pipe_cat_stdin, "interval\n");
            flush();
            sleep(1);
        } while (true);
        
        break;
    default : // we are the parent
        // do nothing
        return $pid_interval;
    } 
}
function content_walk($monitor, $pipe_lemon_stdin)
{       
    $descriptorspec = array(
        0 => array('pipe', 'r'),  // stdin
        1 => array('pipe', 'w'),  // stdout
        2 => array('pipe', 'w',)  // stderr
    );
    
    $proc_cat = proc_open('cat', $descriptorspec, $pipe_cat);

    content_event_idle($pipe_cat[0]);
    content_event_interval($pipe_cat[0]);

    while(!feof($pipe_cat[1])) {
        $event = trim(fgets($pipe_cat[1]));
        handle_command_event($monitor, $event);
        
        $text = get_statusbar_text($monitor);
        fwrite($pipe_lemon_stdin, $text."\n");
        flush();
    }

    pclose($pipe_cat[1]);
}

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.

function detach_lemon_conky($parameters)
{ 
    $pid_conky = pcntl_fork();

    switch($pid_conky) {         
    case -1 : // fork errror         
        die('could not fork');
    case 0  : // we are the child
        $cmd_out  = 'lemonbar '.$parameters;
        $pipe_out = popen($cmd_out, "w");

        $path     = __dir__."/../conky";
        $cmd_in   = 'conky -c '.$path.'/conky.lua';
        $pipe_in  = popen($cmd_in,  "r");
    
        while(!feof($pipe_in)) {
            $buffer = fgets($pipe_in);
            fwrite($pipe_out, $buffer);
            flush();
        }
    
        pclose($pipe_in);
        pclose($pipe_out);

        break;
    default : // we are the parent             
        return $pid_conky;
    }  
}

And execute the function main script in panel.pl.

#!/usr/bin/php 
<?php # using PHP7

require_once(__DIR__.'/helper.php');
require_once(__DIR__.'/pipehandler.php');

// main

$panel_height = 24;
$monitor = get_monitor($argv);

system('pkill lemonbar');
system("herbstclient pad $monitor $panel_height 0 $panel_height 0");

// run process in the background

$params_top = get_params_top($monitor, $panel_height);
detach_lemon($monitor, $params_top);

$params_bottom = get_params_bottom($monitor, $panel_height);
detach_lemon_conky($params_bottom);

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.

function kill_zombie()
{
    system('pkill -x dzen2');
    system('pkill -x lemonbar');
    system('pkill -x cat');
    system('pkill conky');
    system('pkill herbstclient');
}

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 !