Haskell 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 ]


Pipe and Fork in Desktop Ricing

Why Haskell? 

This is not a scripting language but compiled. The reason is XMonad. Most people is still combined the Haskell Configuration, with BASH, conky, even Perl. Makes it looks like a Frankestein. We need a more unified approach.

And of course you have total Control of the script, e.g. color across the configuration, when you call from just one Haskell script, instead of many language.

Be Lazy, Learn 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.

A few things to be noted here:

  • Haskell does have threadDelay function. But since it is in microsecond. We have to wrap it in a new sleep function.

  • Haskell complex time library could be wrapped into simple function.

  • Haskell has no built in loop. But we can use Control, e.g. Forever.

Source:

import Data.Time.LocalTime
import Data.Time.Format

import Control.Concurrent
import Control.Monad

-- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ---
-- wrap Funktion

myTimeFormat = "%a %b %d %H:%M:%S"

wFormatTime :: FormatTime t => t -> String
wFormatTime myUtcTime = formatTime 
    Data.Time.Format.defaultTimeLocale myTimeFormat myUtcTime

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

printDate = do
    now <- getZonedTime
    let nowFmt = wFormatTime now
    putStrLn nowFmt
    wSleep 1

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

main = forever $ printDate

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

Pipe: Basic


How does it works ?

Haskell has no built in loop. Since we want infinite loop, we use Control Forever.

main = forever $ printDate

There is this Dollar $ operator. You might consider reading my old post.


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

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:

import System.Process
import System.Directory

-- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ---
-- wrap Funktion

-- source directory is irrelevant in Haskell
-- but we'll do it anyway for the sake of learning
wGetCmdIn :: String -> String
wGetCmdIn dirname = "conky -c " 
    ++ dirname ++ "/../assets" ++ "/conky.lua"

cmdout = "less" -- or dzen2

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

main = do
    dirname <- getCurrentDirectory
    let cmdin = wGetCmdIn dirname
    system $ cmdin ++ " | " ++ 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 dirname, relative to the Haskell source, to locate the conky script assets. Source directory is irrelevant in Haskell, since Haskell is compiled, and can be execute from anywhere. But we’ll do it anyway for the sake of learning

We use this amazingly cool Haskell’s createProcess mechanism.

Source:

import System.Process
import System.Directory

-- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ---
-- wrap Funktion

-- Source directory is irrelevant in Haskell
-- but we'll do it anyway for the sake of learning
wConkyFileName :: String -> String
wConkyFileName dirName = dirName ++ "/../assets" ++ "/conky.lua"

cmdin  = "conky"
cmdout = "less" -- or dzen2

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

main = do
    dirName <- getCurrentDirectory
    let conkyFileName = wConkyFileName dirName   
  
    (_, Just pipeout, _, _) <- 
        createProcess (proc cmdin ["-c", conkyFileName])
        { std_out = CreatePipe } 

    (_, _, _, ph)  <- 
        createProcess (proc cmdout []) 
        { std_in = UseHandle pipeout }
    
    -- just remove this waitForProcess line to detach dzen2
    _ <- waitForProcess ph

    putStr ""

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

You can just remove this waitForProcess line to detach dzen2. No need to fork

Pipe: to Less

Your wallpaper might be different than mine.

How does it works ?

First process create a new stdout handle std_out = CreatePipe. And the second process use it as stdin feed std_in = UseHandle pipeout.

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

    (_, _, _, ph)  <- 
        createProcess (proc cmdout []) 
        { std_in = UseHandle pipeout }

A Unidirectional Pipe from Internal Function

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

Do not forget to flush.

Source:

import System.Process
import System.IO

import Data.Time.LocalTime
import Data.Time.Format

import Control.Concurrent
import Control.Monad

-- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ---
-- wrap Funktion

wFormatTime :: FormatTime t => t -> String
wFormatTime myUtcTime = formatTime 
    Data.Time.Format.defaultTimeLocale myTimeFormat myUtcTime
  where myTimeFormat = "%a %b %d %H:%M:%S"

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

generatedOutput pipein = do
     now <- getZonedTime
     let nowFmt = wFormatTime now

     hPutStrLn pipein nowFmt
     hFlush pipein

     wSleep 1

cmdout = "less" -- or dzen2

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

main = do
    (Just pipein, _, _, ph)  <- 
        createProcess (System.Process.proc cmdout []) 
        { std_in = CreatePipe }
    
    forever $ generatedOutput pipein
    hClose pipein
    
    putStr ""

How does it works ?

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

The second process use its own stdin std_in = CreatePipe.

     now <- getZonedTime
     let nowFmt = wFormatTime now

     hPutStrLn pipein nowFmt

Fork Overview

This step use internal function as source feed, as continuation of previous step.

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:

import System.Process
import System.IO
import System.Posix.Process

import Data.Time.LocalTime
import Data.Time.Format

import Control.Concurrent
import Control.Monad

-- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ---
-- wrap Funktion

cmdout = "dzen2"

getDzen2Parameters = [
      "-x", xpos,  "-y", ypos,
      "-w", width, "-h", height,
      "-fn", font,
      "-ta", "c",
      "-bg", bgcolor,
      "-fg", fgcolor,
      "-title-name", "dzentop"
    ]
  where    
    xpos    = "0"
    ypos    = "0"
    width   = "640"
    height  = "24"
    fgcolor = "#000000"
    bgcolor = "#ffffff"
    font    = "-*-fixed-medium-*-*-*-12-*-*-*-*-*-*-*"


wFormatTime :: FormatTime t => t -> String
wFormatTime myUtcTime = formatTime 
    Data.Time.Format.defaultTimeLocale myTimeFormat myUtcTime
  where myTimeFormat = "%a %b %d %H:%M:%S"



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

generatedOutput pipein = do
     now <- getZonedTime
     let nowFmt = wFormatTime now

     hPutStrLn pipein nowFmt
     hFlush pipein

     wSleep 1

runDzen2 = do
    (Just pipein, _, _, ph)  <- 
        createProcess (proc cmdout getDzen2Parameters) 
        { std_in = CreatePipe }
    
    forever $ generatedOutput pipein
    
    hClose pipein

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

main = do
    -- remove all dzen2 instance
    system "pkill dzen2"

    -- run process in the background
    -- forkProcess is required since we use forever control
    forkProcess $ runDzen2
    
    putStr ""

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 ?

Since we use forever control, it will block the process forever. So we do have to detach it using forkProcess.

    forkProcess $ runDzen2

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:

import System.Process
import System.Directory
import System.IO
import System.Posix.Process
import System.Posix.Types

import Control.Concurrent
import Control.Monad

-- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ---
-- wrap Funktion

cmdin  = "conky"
cmdout = "dzen2"

-- Source directory is irrelevant in Haskell
-- but we'll do it anyway for the sake of learning
wConkyFileName :: String -> String
wConkyFileName dirName = dirName ++ "/../assets" ++ "/conky.lua"

getDzen2Parameters :: [String]
getDzen2Parameters = [
      "-x", xpos,  "-y", ypos,
      "-w", width, "-h", height,
      "-fn", font,
      "-ta", "c",
      "-bg", bgcolor,
      "-fg", fgcolor,
      "-title-name", "dzentop"
    ]
  where    
    xpos    = "0"
    ypos    = "0"
    width   = "640"
    height  = "24"
    fgcolor = "#000000"
    bgcolor = "#ffffff"
    font    = "-*-fixed-medium-*-*-*-12-*-*-*-*-*-*-*"

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

detachDzen2 :: IO ()
detachDzen2 = do
    dirName <- getCurrentDirectory
    let conkyFileName = wConkyFileName dirName  

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

    (_, _, _, ph)  <- 
        createProcess (proc cmdout getDzen2Parameters) 
        { std_in = UseHandle pipeout }
      
    hClose pipeout

detachTransset :: IO ProcessID
detachTransset = forkProcess $ do    
    wSleep 1
    system "transset .8 -n dzentop >/dev/null"
    return ()

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

main = do
    -- remove all dzen2 instance
    system "pkill dzen2"

    -- run process in the background
    detachDzen2

    -- optional transparency
    detachTransset
    
    return ()

This would have dzen2 output similar to this below.

Pipe: to Dzen2

You may use transset-df instead of transset.

How does it works ?

Since we do not use forever control, we do not need to detach dzen2 using forkProcess.

But we still have to do forkProcess for transset. So it does not wait for wSleep 1. Or to be precise, delayed in the background.

import System.Posix.Types

detachTransset :: IO ProcessID
detachTransset = forkProcess $ do    
    wSleep 1
    system "transset .8 -n dzentop >/dev/null 2"
    return ()

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.