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
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.
Table of Content
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.
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
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.
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.