Ruby Plumber.

Goal: A script that continuously show date and time, with Dzen2, and Conky.

Before you dip your toe to scripting, you might desire to know the reason by reading this overview.

Reading


Piping and Forking in Many Languages

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

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

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


Start Simple

Welcome to n00berland. Begin with simple script. We will use this loop as a source feed to pipe. This step won’t introduce Pipe nor Fork.

This script only show an infinite loop showing local time. Each updated in one second interval. We manage this interval by delaying, using sleep code.

Source:

#!/usr/bin/ruby

timeformat = '%a %b %d %H:%M:%S'

while true do
  localtime = Time.now
  datestr = localtime.strftime(timeformat)
  puts datestr

  sleep(1)
end

Call to this simple code would produce time marching, one after another, below the command line prompt.

Pipe: Basic


External Command as Source Feed

Beside previous simple loop that is used as Internal Command, this tutorial also provide Conky as External Command in asset directory. I made it as simple as possible.

Source:

conky.config = {
    out_to_x = false,
    out_to_console = true,
    short_units = true,
    update_interval = 1
}

conky.text = [[\
${time %a %b %d %H:%M:%S}\
]]

Spawning Using System Shell

Ruby provide a dozen Pipe mechanism. Here we have system, spawn, and shell.transact. These three related to system shell.

Using system shell is simple and straightforward. But it does not have any ability, to stream internal function process, that required later on this article.

Source:

Using system:

#!/usr/bin/ruby

path    = __dir__+ "/../assets"
cmdin   = 'conky -c ' + path + '/conky.lua'
cmdout  = 'less' # or 'dzen2'
cmd     = cmdin + ' | ' + cmdout

system(cmd

Using spawn:

#!/usr/bin/ruby

path    = __dir__+ "/../assets"
cmdin   = 'conky -c ' + path + '/conky.lua'
cmdout  = 'dzen2' 

read, write = IO.pipe

spawn(cmdin, out: write)
spawn(cmdout, in: read )

write.close

Using shell.transact:

#!/usr/bin/ruby

require 'shell'

path    = __dir__+ "/../assets"
cmdin   = 'conky -c ' + path + '/conky.lua'
cmdout  = 'dzen2'

sh = Shell.new

sh.transact { system(cmdin) | system(cmdout) }

A Unidirectional Pipe Between External Command

This step is overview of Pipe between two external command. This short script is using conky as pipe source feed and less as pipe target. Showing time and date forever in the console.

This infinite pipe run in time-less fashioned.

I add _ dir _, relative to the Ruby source, to locate the conky script assets.

We use this flexible Ruby’s IO.popen mechanism.

Source:

#!/usr/bin/ruby

# http://ruby-doc.org/core-1.8.7/IO.html#method-c-popen

def generated_output(stdin)
  path    = __dir__+ "/../assets"
  cmdin   = 'conky -c ' + path + '/conky.lua'
    
  IO.popen(cmdin, "r") do |f| 
    while f do
      stdin.puts f.gets
    end
    f.close()    
  end
end

cmdout  = 'less' # or 'dzen2'

IO.popen(cmdout, "w") do |f| 
    generated_output(f) 
        
    f.close()    
end

You can see, how simple it is. This would have less output similar to this below.

Pipe: to Less

Your wallpaper might be different than mine.

How does it works ?

First IO.popen using cmdout create stdin. Second IO.popen using cmdin create stdout. Ruby act as middle man, stdout read by Ruby, and put in stdin.

    IO.popen(cmdin, "r") do |f| 
        while f do
          stdin.puts f.gets
        end
        f.close()    
    end

A Unidirectional Pipe from Internal Function

Again, Ruby provide a dozen Pipe mechanism. Here we have Open3.pipeline_w, IO.pipe, IO.popen, and PTY.spawn. These three related unidirectional capability that we need. Or to be precise, they have bidirectional capability.

Using internal function as source feed to external command is straight forward. This should be self explanatory.

Do not forget to flush.

Source:

Using IO.pipe:

#!/usr/bin/ruby

# https://ruby-doc.org/core-2.2.0/Process.html#method-c-spawn

def generated_output(stdin)
  timeformat = '%a %b %d %H:%M:%S'

  while true do
    localtime = Time.now
    datestr = localtime.strftime(timeformat)
    stdin.puts datestr

    sleep(1)
  end
end

cmdout  = 'less' # or 'dzen2'

rd, wr = IO.pipe

if fork
  wr.close
  spawn(cmdout, in: rd )
  rd.close
  Process.wait
else
  rd.close
  generated_output(wr)
  wr.close
end

Using IO.popen:

#!/usr/bin/ruby

# http://ruby-doc.org/core-1.8.7/IO.html#method-c-popen

def generated_output(stdin)
  timeformat = '%a %b %d %H:%M:%S'

  while true do
    localtime = Time.now
    datestr = localtime.strftime(timeformat)
    stdin.puts datestr

    sleep(1)
  end
end

cmdout  = 'less' # or 'dzen2'

IO.popen(cmdout, "w") { |f| generated_output(f) }

Using Open3.pipeline_w:

#!/usr/bin/ruby

# http://ruby-doc.org/stdlib-2.4.1/libdoc/open3/rdoc/Open3.html#method-c-pipeline_w

require 'open3'

def generated_output(stdin)
  timeformat = '%a %b %d %H:%M:%S'

  while true do
    localtime = Time.now
    datestr = localtime.strftime(timeformat)
    stdin.puts datestr

    sleep(1)
  end
end

cmdout  = 'less' # or 'dzen2'

Open3.pipeline_w(cmdout) {|i, ts| generated_output(i) }

Using PTY.spawn:

#!/usr/bin/ruby

# https://ruby-doc.org/stdlib-2.2.3/libdoc/pty/rdoc/PTY.html

require 'pty'

def generated_output(stdin)
  timeformat = '%a %b %d %H:%M:%S'

  while true do
    localtime = Time.now
    datestr = localtime.strftime(timeformat)
    stdin.puts datestr

    sleep(1)
  end
end

cmdout  = 'dzen2' 

PTY.spawn(cmdout) { |output, input, pid| generated_output(input) }

How does it works ?

The same as previous. But instead of reading from stdout, it is managed by internal process using stdin.puts.

    datestr = localtime.strftime(timeformat)
    stdin.puts datestr

Fork Overview

This step use internal function as source feed, as continuation of previous step. To avoid complexity of longer script, most code is written inside function

This step use dzen2, with complete parameters. This dzen2 is forked, running in the background. Detached from the script, no need to wait for dzen2 to finish the script.

Source:

#!/usr/bin/ruby

def get_dzen2_parameters()
  xpos    = '0'
  ypos    = '0'
  width   = '640'
  height  = '24'
  fgcolor = '#000000'
  bgcolor = '#ffffff'
  font    = '-*-fixed-medium-*-*-*-12-*-*-*-*-*-*-*'

  parameters  = "  -x #{xpos} -y #{ypos} -w #{width} -h #{height}"
  parameters << " -fn '#{font}'"
  parameters << " -ta c -bg '#{bgcolor}' -fg '#{fgcolor}'"
  parameters << " -title-name dzentop"
end

def generated_output(stdin)
  timeformat = '%a %b %d %H:%M:%S'

  while true do
    localtime = Time.now
    datestr = localtime.strftime(timeformat)
    stdin.puts datestr

    sleep(1)
  end
end

def run_dzen2()
  cmdout  = 'dzen2 ' + get_dzen2_parameters()
  IO.popen(cmdout, "w") do |f| 
    generated_output(f) 
        
    f.close()    
  end
end

def detach_dzen2()
  # warning: Signal.trap is application wide
  Signal.trap("PIPE", "EXIT")
    
  pid = fork { run_dzen2() }
  Process.detach(pid)
end

# ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----
# main

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

# run process in the background
detach_dzen2()

This step also add system command that kill any previous dzen2 instance. So it will be guaranteed, that the dzen2 shown is coming from the latest script.


How does it works ?

Any code inside the fork block executed under new child process with new pid The child process could be detached from parent process using Process.detach(pid).

def detach_dzen2()
    pid = fork { run_dzen2() }
    Process.detach(pid)
end

Polishing The Script

This step, we use conky again, as a source feed. And also parameterized dzen2 as continuation of previous step.

This step add optional transset transparency, detached from script. So we two forks, dzen and transset.

Source:

#!/usr/bin/ruby

def get_dzen2_parameters()
  xpos    = '0'
  ypos    = '0'
  width   = '640'
  height  = '24'
  fgcolor = '#000000'
  bgcolor = '#ffffff'
  font    = '-*-fixed-medium-*-*-*-12-*-*-*-*-*-*-*'

  parameters  = "  -x #{xpos} -y #{ypos} -w #{width} -h #{height}"
  parameters << " -fn '#{font}'"
  parameters << " -ta c -bg '#{bgcolor}' -fg '#{fgcolor}'"
  parameters << " -title-name dzentop"
end

def generated_output(stdin)
  path    = __dir__+ "/../assets"
  cmdin   = 'conky -c ' + path + '/conky.lua'
    
  IO.popen(cmdin, "r") do |f| 
    while f do
      stdin.puts f.gets
    end
  f.close()    
  end
end

def run_dzen2()
  cmdout  = 'dzen2 ' + get_dzen2_parameters()
  IO.popen(cmdout, "w") do |f| 
    generated_output(f) 
        
    f.close()    
  end
end

def detach_dzen2()
  # warning: Signal.trap is application wide
  Signal.trap("PIPE", "EXIT")
    
  pid = fork { run_dzen2() }
  Process.detach(pid)
end

def detach_transset()
  pid = fork do
    sleep(1)
    system('transset .8 -n dzentop >/dev/null')        
  end
    
  Process.detach(pid)
end

# ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----
# main

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

# run process in the background
detach_dzen2()

# optional transparency
detach_transset()

This would have dzen2 output similar to this below.

Pipe: to Dzen2

You may use transset-df instead of transset.

How does it works ?

Nothing new here.

Lemonbar

I also provide Lemonbar, instead of Dzen2. The code is very similar.

Source:


Coming up Next

There already an advance case of Pipe and Fork. Multitier, and bidirectional.


There above are some simple codes I put together. I’m mostly posting codes so I won’t have any problems finding it in the future.

Thank you for reading.