Preface
Goal: Manage Herbstclient process trigerred by idle event
Focusing in "herbstclient --idle".
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.
Table of Content
Reference
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.
-
Lemonbar: gitlab.com/…/dotfiles/…/python/
The PipeHandler Source File:
Let’s have a look at pipehandler.py
in github.
Statusbar Screenshot
Dzen2
Lemonbar
1: Without Idle event
Let’s have a look at our main
panel.py
in github.
At the end of the script, we finally call lemonbar
with detach_lemon
function.
# remove all lemonbar instance
os.system('pkill lemonbar')
# run process in the background
pipehandler.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 = os.fork()
.
def detach_lemon(monitor, parameters):
pid_lemon = os.fork()
if pid_lemon == 0:
try:
run_lemon(monitor, parameters)
os._exit(1)
finally:
import signal
os.kill(pid_lemon, signal.SIGTERM)
The real function is run_lemon
.
You must be familiar with this subprocess.Popen
.
It is very flexible, and support bidirectional pipe that we need later.
def run_lemon(monitor, parameters):
command_out = 'lemonbar ' + parameters + ' -p'
pipe_lemon_out = subprocess.Popen(
[command_out],
stdin = subprocess.PIPE, # for use with content processing
shell = True,
universal_newlines=True
)
content_init(monitor, pipe_lemon_out)
pipe_lemon_out.stdin.close()
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.
def content_init(monitor, pipe_lemon_out):
output.set_tag_value(monitor)
output.set_windowtitle('')
text = output.get_statusbar_text(monitor)
pipe_lemon_out.stdin.write(text + '\n')
pipe_lemon_out.stdin.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.
2: With Idle event
Consider this content_walk
call,
after content_init
call,
inside the run_lemon
.
def run_lemon(monitor, parameters):
command_out = 'lemonbar ' + parameters
pipe_lemon_out = subprocess.Popen(
[command_out],
stdin = subprocess.PIPE, # for use with content processing
shell = True,
universal_newlines=True
)
content_init(monitor, pipe_lemon_out)
content_walk(monitor, pipe_lemon_out) # loop for each event
pipe_lemon_out.stdin.close()
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
.
def content_walk(monitor, pipe_lemon_out):
# start a pipe
command_in = 'herbstclient --idle'
pipe_idle_in = subprocess.Popen(
[command_in],
stdout = subprocess.PIPE,
stderr = subprocess.STDOUT,
shell = True,
universal_newlines = True
)
# wait for each event, trim newline
for event in pipe_cat.stdout:
handle_command_event(monitor, event.strip())
text = output.get_statusbar_text(monitor)
pipe_lemon_out.stdin.write(text + '\n')
pipe_lemon_out.stdin.flush()
pipe_idle_in.stdout.close()
3: The Event Handler
For each idle event, there are multicolumn string. The first string define the event origin.
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
.
def handle_command_event(monitor, event):
# find out event origin
column = event.split("\t")
origin = column[0]
tag_cmds = ['tag_changed', 'tag_flags', 'tag_added', 'tag_removed']
title_cmds = ['window_title_changed', 'focus_changed']
if origin == 'reload':
os.system('pkill lemonbar')
elif origin == 'quit_panel':
exit()
elif origin in tag_cmds:
output.set_tag_value(monitor)
elif origin in title_cmds:
title = column[2] if (len(column) > 2) else ''
output.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.
4: Lemonbar Clickable Areas
This is specific issue for lemonbar, that we don’t have in dzen2.
Consider have a look at
output.py
.
# clickable tags
text_name = '%{A:herbstclient focus_monitor "' \
+ str(monitor) + '" && ' + 'herbstclient use "' \
+ tag_index + '":} ' + tag_name + ' %{A} '
Issue: Lemonbar put the output on terminal instead of executing the command.
Consider going back to
pipehandler.py
.
We need to pipe the lemonbar output to shell. It means Lemonbar read input and write output at the same time.
def run_lemon(monitor, parameters):
command_out = 'lemonbar ' + parameters
pipe_lemon_out = subprocess.Popen(
[command_out],
stdout = subprocess.PIPE, # for use with shell, note this
stdin = subprocess.PIPE, # for use with content processing
shell = True,
universal_newlines=True
)
pipe_sh = subprocess.Popen(
['sh'],
stdin = pipe_lemon_out.stdout,
shell = True,
universal_newlines=True
)
content_init(monitor, pipe_lemon_out)
content_walk(monitor, pipe_lemon_out) # loop for each event
pipe_lemon_out.stdin.close()
pipe_lemon_out.stdout.close()
How does it work ?
Just pay attention to these lines.
We are using pipe_lemon_out.stdout
as a feed to pipe_sh.stdin
.
pipe_lemon_out = subprocess.Popen(
stdout = subprocess.PIPE, # for use with shell, note this
stdin = subprocess.PIPE, # for use with content processing
)
pipe_sh = subprocess.Popen(
stdin = pipe_out.stdout,
)
View Source File:
Piping lemonbar output to shell, implementing lemonbar clickable area.
5: 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. Luckily we can treat interval as event.
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.
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.
6: Combined Event
Preparing The View
This is what it looks like, an overview of what we want to achieve.
Consider make a progress in
output.py
.
segment_datetime = '' # empty string
def get_statusbar_text(monitor):
...
# draw date and time
text += '%{c}'
text += output_by_datetime()
...
def output_by_datetime():
return segment_datetime
def set_datetime():
...
segment_datetime = date_text + ' ' + time_text
And a few enhancement in
pipehandler.py
.
def handle_command_event(monitor, event):
...
if origin == 'reload':
...
elif origin == 'interval':
output.set_datetime()
def content_init(monitor, pipe_lemon_out):
...
output.set_windowtitle('')
output.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 ofcat
process. -
content_event_idle
: HerbstluftWM idle event. Forked, as background processing. -
content_event_interval
: Custom date time event. Forked, as background processing.
def content_event_idle(pipe_cat_out):
pid_idle = os.fork()
if pid_idle == 0:
try:
# start a pipe
command_in = 'herbstclient --idle'
pipe_idle_in = subprocess.Popen(
[command_in],
stdout = subprocess.PIPE,
stderr = subprocess.STDOUT,
shell = True,
universal_newlines = True
)
# wait for each event
for event in pipe_idle_in.stdout:
pipe_cat_out.stdin.write(event)
pipe_cat_out.stdin.flush()
pipe_idle_in.stdout.close()
finally:
import signal
os.kill(pid_idle, signal.SIGTERM)
def content_event_interval(pipe_cat_out):
pid_interval = os.fork()
if pid_interval == 0:
try:
while True:
pipe_cat_out.stdin.write("interval\n")
pipe_cat_out.stdin.flush()
time.sleep(1)
finally:
import signal
os.kill(pid_interval, signal.SIGTERM)
def content_walk(monitor, pipe_lemon_out):
pipe_cat = subprocess.Popen(
['cat'],
stdin = subprocess.PIPE,
stdout = subprocess.PIPE,
shell = True,
universal_newlines=True
)
content_event_idle(pipe_cat)
content_event_interval(pipe_cat)
# wait for each event, trim newline
for event in pipe_cat.stdout:
handle_command_event(monitor, event.strip())
text = output.get_statusbar_text(monitor)
pipe_lemon_out.stdin.write(text + '\n')
pipe_lemon_out.stdin.flush()
pipe_cat.stdin.close()
pipe_cat.stdout.close()
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.
7: 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:
-
Lemonbar:
We only need one function to do this in
pipehandler.pm
.
def detach_lemon_conky(parameters):
pid_conky = os.fork()
if pid_conky == 0:
try:
dirname = os.path.dirname(os.path.abspath(__file__))
path = dirname + "/../conky"
cmd_in = 'conky -c ' + path + '/conky.lua'
cmd_out = 'lemonbar ' + parameters
pipe_out = subprocess.Popen(
[cmd_out],
stdin = subprocess.PIPE,
shell = True,
universal_newlines=True
)
pipe_in = subprocess.Popen(
[cmd_in],
stdout = pipe_out.stdin,
stderr = subprocess.STDOUT,
shell = True,
universal_newlines = True
)
pipe_out.stdin.close()
outputs, errors = pipe_out.communicate()
# avoid zombie apocalypse
pipe_out.wait()
os._exit(1)
finally:
import signal
os.kill(pid_conky, signal.SIGTERM)
And execute the function main script in
panel.pl
.
#!/usr/bin/env python3
import os
import sys
import helper
import pipehandler
# main
panel_height = 24
monitor = helper.get_monitor(sys.argv)
os.system('pkill lemonbar')
os.system('herbstclient pad ' + str(monitor) + ' '
+ str(panel_height) + ' 0 ' + str(panel_height) + ' 0')
# run process in the background
params_top = helper.get_params_top(monitor, panel_height)
pipehandler.detach_lemon(monitor, params_top)
params_bottom = helper.get_params_bottom(monitor, panel_height)
pipehandler.detach_lemon_conky(params_bottom)
View Source File:
Dual Bar, detach_lemon_conky
function.
8: 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.
def kill_zombie():
os.system('pkill -x dzen2')
os.system('pkill -x lemonbar')
os.system('pkill -x cat')
os.system('pkill conky')
os.system('pkill herbstclient')
9: 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.
Enjoy the statusbar ! Enjoy the window manager !