@hackage dzen-dhall1.0.2

Configure dzen2 bars in Dhall language

dzen-dhall

Build Status Hackage

Dzen is a general purpose messaging, notification and menuing program for X11. It features rich in-text formatting & control language, allowing to create GUIs by piping output of arbitrary executables to the dzen2 binary. There are plenty of good usage examples on r/unixporn.

Unfortunately, combining outputs of multiple executables before feeding them to dzen2, which is usually done by custom shell scripts, is a tedious and error-prone task. Consider the following problems:

Use of newlines

By default, dzen2 only renders the last line of its input, so newlines must be handled somehow by the user.

Complexity of dynamic text formatting

If one wants each program's output to appear on its own fixed position on the screen, trimming and padding the output of each executable is required, to make sure that the text will not jitter when combined.

High delays

Some output sources (shell scripts or commands used to provide the data) take too long to produce the output, some change their outputs rarely, but some are expected to update very frequently (like those that output current time or volume indicators on your screen). It means that the while true; do ...; done | dzen2 pattern is not ideal. Some clever scheduling should be done to avoid delays and excessive resource waste. Output sources should be ran in parallel with their own update intervals.

No code reuse

It is hard to share pieces of code used to produce output in dzen2 markup format because of the need to adapt the code. Ideally, there should be a "plugin system" allowing to import reusable configurations with a single command.

Non-trivial markup is hard

Dzen in-text format and control language is quite rich: it features almost-arbitrary text positioning, text coloring, drawing simple shapes, loading XBM images and even allows to define clickable areas. However, these control structures are too low-level: implementing UI elements we want to use (for example, marquee-like blocks with arbitrary content) would require too much effort. Besides, one more problem with this markup language is that nested tags are not supported.

To fill the abstraction gap, a new DSL should be introduced. This language should allow its users to abstract away from markup-as-text and focus on markup-as-syntax-tree instead - no need to say, tree structures are more suitable for the purpose of defining UIs. It is also way easier to process tree representations programmatically.

The solution

Dhall is a statically-typed total functional programming language. These properties make it a good choice for dealing with complex user-defined configurations: static typing allows to catch typos and errors early, and totality guarantees that a configuration program will always terminate.

This repository contains data type and function definitions in Dhall that form a DSL for creating almost arbitrary Dzen UIs, called "bars", and a Haskell program capable of reading bar definitions and producing input for dzen2 binary based on them.

In effect, dzen-dhall introduces a new approach to desktop scripting/customization with Dzen. Basically, it provides solutions for all of the aforementioned problems. dzen-dhall is smart when formatting text, handles newlines gracefully, runs output sources in parallel, and its plugin system solves the problem of code reuse.

A quick example

The essence of the DSL can be illustrated by the following excerpt from the default config file (with additional comments):

-- A bar that shows how much memory is used:
let memoryUsage
-- ^ `let` keyword introduces a new binding
	: Bar
	-- ^ Colon means "has type". `memoryUsage` is a `Bar`
	= bashWithBinaries
	  -- ^ Call to a function named `bashWithBinaries` with three arguments:
	  [ "free", "grep", "awk" ]
	  -- ^ A list of binaries required to run the script (used to exit early if
	  -- some of them are not present).
	  5000
	  -- ^ Update interval in milliseconds
	  ''
	  free -b | grep Mem | awk '{ printf("%.0f\n", $3 * 100 / $2) }';
	  ''
	  -- ^ The script itself

-- A bar that shows how much swap is used:
let swapUsage
	: Bar
	= bashWithBinaries
	  [ "free", "grep", "awk" ]
	  5000
	  ''
	  free -b | grep Swap | awk '{ printf("%.0f\n", $3 * 100 / $2) }';
	  ''

-- A bar that shows current date:
let date
	: Bar
	= bashWithBinaries [ "date" ] 1000 "date +'%d.%m.%Y'"

-- A bar that shows current time:
let time
	: Bar
	= bashWithBinaries [ "date" ] 1000 "date +'%H:%M'"

-- A function that colorizes a given `Bar`:
let accent : Bar → Bar = fg "white"

in	separate
    -- ^ a function that inserts |-separators between nearby elements of a list
	[ join [ text "Mem: ", accent memoryUsage, text "%" ]
	-- ^ `join` concatenates multiple `Bar`s
	, join [ text "Swap: ", accent swapUsage, text "%" ]
		  -- ^ `text` is used to convert a text value to a `Bar`
	, join [ date, text " ", accent time ]
	] : Bar

This definition results in the following Dzen output:

Example 1

Getting started

Building

Using stack

stack build

Using Nix

nix-build --attr dzen-dhall

To use pinned version of nixpkgs, pass --arg usePinned true.

Installing

Binary releases are available.

Using stack

stack install

Using Nix

nix-env --file default.nix --install dzen-dhall

To use pinned version of nixpkgs, pass --arg usePinned true.

Running

To create a default configuration, run:

dzen-dhall init

dzen-dhall will put some files to ~/.config/dzen-dhall/

Files in types/ and utils/ subdirectories are set read-only by default - the user should not edit them, since they contain the implementation. They are still exposed to simplify learning and debugging. prelude/ directory contains a copy of Dhall prelude.

Installing plugins

dzen-dhall comes with a plugin system capable of pulling pieces of Dhall code with metadata either from a curated set of plugins or from third-party sources.

For example, let's install a plugin named tomato, which is a countdown timer with interactive UI.

Running dzen-dhall plug tomato will result in fetching the plugin source from this file and pretty-printing it to the terminal for review. You will be prompted for confirmation, and if you confirm the installation, you will see the following output:

New plugin "tomato" can now be used as follows:

let tomato = (./plugins/tomato.dhall).main

in  plug
  ( tomato
        ''
        notify-send --urgency critical " *** Time is up! *** "
        ''
  )

This is a message the author left for you, to demonstrate how to actually use their plugin.

Navigate to your config.dhall, find a comment saying You can add new plugins right here and insert the expression from above instead of the comment (don't forget to also add a leading comma).

After running dzen-dhall again, you should be able to see the output of the newly installed plugin.

Modifying configuration

This chapter describes dzen-dhall DSL in depth. It's best to read the Dhall wiki to become familiar with Dhall syntax before you proceed.

Bars

The most important concept of the DSL is Bar. Essentially, Bar is a tree data structure containing text, images, shapes, etc. in its leaves. Default config file exposes some functions for working with Bars:

-- Text primitives:
let text : Text → Bar
let markup : Text → Bar

-- Used to combine multiple Bars into one. let join : List Bar → Bar

-- Primitives of Dzen markup language: let fg : Color → Bar → Bar let bg : Color → Bar → Bar let i : Image → Bar let r : Natural → Natural → Bar let ro : Natural → Natural → Bar let c : Natural → Bar let co : Natural → Bar let p : Position → Bar → Bar let pa : AbsolutePosition → Bar → Bar let ca : Button → Shell → Bar → Bar let ib : Bar → Bar

-- Animations let slider : Slider → List Bar → Bar let marquee : Marquee → Bar → Bar

-- Other let pad : Natural → Padding → Bar → Bar let trim : Natural → Direction → Bar → Bar let source : Source → Bar let plug : Plugin → Bar let automaton : Text → StateTransitionTable → StateMap Bar → Bar let check : List Check → Bar let define : Variable → Text → Bar let scope : Bar → Bar

Text primitives

text is used to create Bars containing static, escaped pieces of text. markup, on the contrary, does not escape its input, so that if it does contain markup, it will be interpreted by dzen2.

Primitives

Various primitives of dzen2 markup language (^fg(), ^bg(), ^i(), etc. - see dzen2 README for details on them) are represented by corresponding Bar constructors (fg, bg, i, etc.).

Coloring

Background and foreground colors can be set using bg and fg. A color can be one of the following:

  • X11 color name;
  • #XXX-formatted hex number;
  • #XXXXXX-formatted hex number.

fg can also be used to set colors of XBM bitmaps.

dzen-dhall color blocks can be nested (unlike when using plain dzen2 markup). ^fg(red) red ^fg(pink) pink ^fg() red ^fg() will be rendered by dzen2 as . But dzen-dhall will process fg "red" (join [ text "red ", fg "pink" (text "pink"), text " red" ]) as .

Ignoring background color

ib can be used to completely disable background coloring for a region of output. Background coloring can't be enabled again from within a child Bar.

For example, this:

bg
("#F00")
( join
[ text "..."
, ib
  ( join
	[ text "background color is "
	, bg ("#0F0") (text "completely")
	, text "ignored"
	]
  )
, text "..."
]
)

results in the following output:

Ignore background preview

Drawing images

XBM bitmaps can be loaded using i function.

i accepts both filenames and raw image contents (XBM images are just pieces of C code):

For example,

i
''
#define smiley_width 15
#define smiley_height 15
static unsigned char smiley_bits[] = {
   0x00, 0x00, 0x00, 0x00, 0xc8, 0x00, 0xcc, 0x00, 0x4c, 0x00, 0x48, 0x00,
   0x00, 0x00, 0x00, 0x08, 0x00, 0x08, 0x04, 0x04, 0x3c, 0x07, 0xe0, 0x01,
   0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
''

will be rendered as smiley.

To edit/create XBM images, use GIMP.

Drawing shapes

Four types of shapes are supported:

Function Meaning
r Rectangle
ro Rectangle outline
c Circle
co Circle outline

Relative positioning

Relative positioning (p) allows to shift by some number of pixels in any direction, reset vertical position, lock or unlock horizontal position, or move to one of the four edges of the screen:

let Position
	: Type
	= < XY :
		  { x : Integer, y : Integer }
	  | _RESET_Y
	  | _LOCK_X
	  | _UNLOCK_X
	  | _LEFT
	  | _RIGHT
	  | _TOP
	  | _CENTER
	  | _BOTTOM
	  >

For example, (p (Position.XY { x = +10, y = -5 }) (text "Relative position")).

Absolute positioning

With pa function, it is possible to specify absolute position of a bar, relative to the top-left corner of the screen.

AbsolutePosition is defined as:

let AbsolutePosition : Type = { x : Integer, y : Integer }

Example:

(pa { x = +0, y = +0 } (text "Absolute position"))

Clickable areas

Example:

(ca Button.Left "notify-send hello!" (text "Click me!"))

dzen2 does not allow a command in ^ca() tag to contain closing parentheses, because ) is used to indicate the end of the command. dzen-dhall bypasses this limitation:

(ca Button.Left "notify-send '(even with parentheses)'" (text "Click me!"))
Buttons

Button is defined as:

let Button : Type = < Left | Middle | Right | ScrollUp | ScrollDown | ScrollLeft | ScrollRight >

Animations

Some built-in animations are available. More may be added in the future.

Sliders

Sliders change their outputs, variating between Bars from a given list. Transitions are rendered smoothly.

E.g., the following piece:

let fadeIn : Fade = mkFade VerticalDirection.Up 5 16
       -- How many frames to spend on switching ^

let fadeOut : Fade = mkFade VerticalDirection.Down 5 16
    -- How many pixels up or down to move the output ^

in	slider
	(mkSlider fadeIn fadeOut 3000)
                -- Delay, ms ^

	[ join [ text "Mem: ", accent memoryUsage, text "%" ]
	, join [ text "Swap: ", accent swapUsage, text "%" ]
	]
	: Bar

[view complete example]

results in this output:

Slider preview

Marquees

Marquee animation type is inspired by the deprecated marquee HTML tag.

This example with multiple marquees shows how various settings affect appearance:

separate
[ marquee
  (mkMarquee 5 15 False)
          -- ^ Number of animation frames per character.
  ( text
    "The most annoying HTML code in the history of HTML codes."
  )

  , marquee
  (mkMarquee 0 32 True)
            -- ^ Number of characters to show.
  ( text
    "The most annoying HTML code in the history of HTML codes."
  )

  , marquee (mkMarquee 3 10 True) (text "test...")
                         -- ^ Whether to repeat the input indefinitely
                         -- if it is too short, or just pad it with spaces
  , marquee (mkMarquee 3 10 False) (text "test...")

  -- A demo with colors:
  , marquee
  (mkMarquee 8 15 False)
  ( join
    [ text "The "
    , fg "white" (text "most ")
    , text "annoying "
    , fg "white" (text "HTML ")
    , text "code "
    , fg "white" (text "in ")
    , text "the "
    , fg "white" (text "history ")
    , text "of "
    , fg "white" (text "HTML ")
    , text "codes. "
    ]
  )
]

[view complete example]

The output:

Marquee preview

Obviously, marquees and sliders can be nested within each other.

Padding text

Paddings allow to make sure that the width of a piece of text is no less than some number of characters.

let Padding : Type = < Left | Right | Sides >

Example:

(pad 30 Padding.Sides (text "...")) : Bar

Trimming text

trim function allows to cut a given Text to desired width, removing excessive characters from either left or right.

Trim direction is defined as follows:

let Direction = < Left | Right >

Example:

(trim 5 Direction.Right (text "Some long text..."))

Sources

Sources are arbitrary commands that generate text output for dzen-dhall.

let Source : Type =
  { command : List Text
  , input : Text
  , updateInterval : Optional Natural
  , escape : Bool
  }

For example, a simple clock plugin can be created as follows:

let clocks : Source =
  { updateInterval = Some 1000
  , command = ["date", "+%H:%M"]
  , input = ""
  , escape = True
  }

updateInterval specifies minimum update interval: new source command will not be spawned if a previous one is still running (this is done to avoid race conditions). Actual time intervals between source command invocations are adjusted to be as close to specified updateIntervals as possible. For example, if running a command takes 100ms and updateInterval is set to 1000, the real delay between command's exit and startup will be 900ms. And if it takes more than 1000ms, then the real delay will be zero.

If updateInterval is not specified (i.e. set to None Natural), the command will run once. It may continue generating output indefinitely, line-by-line, or exit - in the latter case, the last line of the output will be shown forever.

Note that in most cases it's better to use bash or bashWithBinaries functions instead of constructing sources by hand.

Variables

Sources, hooks and clickable areas can access and modify scope-local variables, using mkVariable, define, set and get functions.

let mkVariable : Text → Variable
let define : Variable → Text → Bar
let get : Variable → Shell
let set : Variable → Shell → Shell

The last two don't actually do anything with variables, they rather construct shell commands that do. dzen-dhall works like a template engine for bash scripts.

For example, let's see how a simple stateful counter can be implemented:

let var = mkVariable "MyVariable"

in	join
  [ define var "0"
  -- ^ set a default value (optional)

  , ca
	Button.Left
	''
	shellVar=${get var}
	${set var "$(( shellVar - 1 ))"}
	''
	(text "-")
  -- ^ a button that decreases the value

  , bash 500
    ''
    echo " ${get var} "
    ''
  -- ^ a bar that prints the value

  , ca
	Button.Left
	''
	shellVar=${get var}
	${set var "$(( shellVar + 1 ))"}
	''
	(text "+")
  -- ^ a button that increases the value

  ]

[view complete example]

At run time, it will look like this: .

Scopes

Scopes are used for encapsulation. dzen-dhall guarantees that automata from different scopes are unable to communicate with each other, and that there are no variable collisions between scopes. Parent scopes are completely isolated from child scopes and vice versa.

For example, let's revisit our counter example from README section about variables.

What if we wanted multiple counters to be present on the screen at the same time? Just inserting many of them is not enough: they will be using the same variable.

But wrapping them into separate scopes helps:

let counter =
	  join
	  [ define var "0"
	  , ca
		Button.Left
		''
		shellVar=${get var}
		${set var "\$(( shellVar - 1 ))"}
		''
		(text "-")
	  , bash 500 "echo \" ${get var} \""
	  , ca
		Button.Left
		''
		shellVar=${get var}
		${set var "\$(( shellVar + 1 ))"}
		''
		(text "+")
	  ]

in	join [ scope counter, scope counter ]

[view complete example]

Automata

Each Bar is essentialy a finite-state automaton. States are tagged by Text labels, and transitions are triggered by events (very much like in some functional reactive programming frameworks). In the trivial case, a bar has only one state: you can think of any static Bar as of an automaton with a single state, the name of which is implicit.

A bar with more than one state can be defined by its state transition function (in a form of a list of transitions), a mapping from state labels to Bars (StateMap), which specifies visual representation of the automaton for various states, and a special identifier (Address) used to query the state of the automaton from the outside world.

For example, this code snippet defines a bar that switches between two states, ON and OFF:

let OFF : State = mkState ""
                       -- ^ Empty label means that this state is initial.
let ON : State = mkState "ON"

let Toggle : Event = mkEvent "Toggle"

let address : Address = mkAddress "MY_AUTOMATON"

let stateTransitionTable
	: List Transition
	= [ mkTransition Toggle ON OFF, mkTransition Toggle OFF ON ]

-- Defines which output to render depending on the state:
let stateMap : StateMap Bar
	= [ { state = OFF, bar = text "Switcher is OFF" }
	  , { state = ON,  bar = text "Switcher is ON" }
	  ]

-- A clickable area that reacts to left-clicks by emitting `Toggle` events:
in	ca
	Button.Left
	(emit Toggle)
	(automaton address stateTransitionTable stateMap)

State maps

StateMaps are used to define mappings from states to bars, i.e. they determine what to show depending on the state.

let StateMap : Type → Type = λ(Bar : Type) → List { state : Text, bar : Bar }

in  StateMap

Note that StateMap is parametrized by the Bar type.

Also note that unlike in traditional reactive frameworks, current state of an automaton only determines which Bar is shown, not present in the tree. See this section for more context.

Events

Events can be emitted from within hooks, sources and clickable areas. The only way to react to some event is to use an automaton.

let mkEvent : Text → Event

let emit : Event → Shell

Hooks

Hooks allow to execute arbitrary commands before state transitions of automata. When a hook exits with non-zero code, it prevents its corresponding state transition from happening. So, generally, hooks should only contain commands that exit fast, to prevent excessive delays.

Relevant bindings include:

let Hook
	: Type
	= { command :
		  List Text
	  , input :
		  Text
	  }

let mkBashHook
	: Shell → Hook

let addHook
	: Hook → Transition → Transition

let getEvent : Shell

let getCurrentState : Shell

let getNextState : Shell

For example, The following hook will succeed only if a certain file exists:

let myHook : Hook =
  { command = "bash"
  , input = "[ -f ~/some-file ]"
  }

Hooks can also emit events themselves (this may lead to event storm, so the user should be really careful).

A special value, getEvent, allows to get the name of the event that triggered the hook. Similarly, getCurrentState and getNextState contain values from the corresponding row of a state transition table.

For example, a hook that inspects current event and both states can be added to all transitions of a state transition table from the automata example:

let withInspect
	: Transition → Transition
	= addHook (mkBashHook "notify-send \"${getEvent}: ${getCurrentState} -> ${getNextState}\"")

let stateTransitionTable
	: List Transition
	= prelude.List.map
	  Transition
	  Transition
      -- ^ Type parameters like these are always explicit in Dhall
	  withInspect
	  [ mkTransition Toggle ON OFF, mkTransition Toggle OFF ON ]

[view complete example]

Assertions

Startup-time assertions allow to make sure that some condition is true before proceeding to the execution. It is possible to assert that some binary is in $PATH or that some arbitrary shell command exits successfully:

let Assertion = < BinaryInPath : Text | SuccessfulExit : Text >

A message will be printed to the console on assertion failure. Assertions, when used wisely, greatly reduce debugging time.

For example, this assertion fails if there's no something binary in $PATH:

check
  "Did you miss something?"
  (Assertion.BinaryInPath "something")

And this assertion fails on weekends:

check
  "Not going to work!"
  (Assertion.SuccessfulExit "[[ \$(date +%u) -lt 6 ]]")

[view complete example]

Code structure overview

The image below contains an import tree for config.dhall. It was generated using dhall resolve --dot.

Naming conventions

These conventions are enforced by dzen-dhall as an attempt to lower cognitive noise for users and plugin maintainers.

  • Event names and variables should be written camel-cased, first letter capitalized: TimeHasCome, ButtonClicked, etc.
  • Automata addresses should contain only capital letters, numbers and _.

Troubleshooting

This section is dedicated to fixing problems with your dzen-dhall configurations.

Getting more info about errors

Pass --explain flag to turn on verbose error reporting.

Marquee jittering

Jittering may appear if the value of fontWidth field in your settings is inadequate. It can be fixed by specifying the width manually:

[ { bar = ...
  , settings = defaults.settings ⫽ { fontWidth = 10 }
  }
]

After a few guesses, you should be able to get rid of jittering.

Another possible source of this problem is a non-monospace font being used. Non-monospace fonts are not supported and will never be.

Embedding shell scripts in Dhall

The most straightforward way is to use ./file.sh as Text construct to embed a file as Text literal into the configuration. However, it is not possible when creating reusable plugins, since it is a requirement that each plugin is encapsulated in a single file, and using string interpolation with as Text is impossible too.

So, the following rules apply:

  1. Use \ to escape ${ } in a single-line ("-quoted) string.

  2. Use '' to escape ${ } in a multiline (''-quoted) string. (That is, '' serves as both an escape sequence and a quote symbol).

For example, bash array expansion expression ${arr[ ix ]} should be written as "\${arr[ ix ]}" in a double-quoted string or as '' ''${arr[ ix ]} '' in a multiline string.

See the specification for details.

Running multiple dzen2s simultaneously

It is possible to do so by adding another Bar (some code duplication is hardly avoidable) and adding another entry to the configuration list:

mkConfigs
	  [ { bar = defaultBar, settings = defaults.settings }
	  , { bar =
		    anotherBar
		, settings =
			-- `-xs` dzen2 argument specifies monitor number:
			defaults.settings ⫽ { extraArgs = [ "-xs", "1" ] }
		}
	  ]

Implementation details

Read this section if you want to understand how dzen-dhall works. It is not required if you want to just use the program.

Data encoding

Dhall does not support recursive ADTs (which are obviously required to construct tree-like statusbar configurations), but there is a trick to bypass that, called Boehm-Berarducci encoding.

We use this method in a slightly modified variant: Carrier type is introduced to hide all the constructors in a huge record.

Essentially, our definition of Bar is equivalent to something like the following, which is a direct Boehm-Berarducci encoding:

let Bar =
      ∀(Bar : Type)
    → ∀(text : Text → Bar)
    → ∀(markup : Text → Bar)
    → ∀(join : List Bar → Bar)
    -- ... some constructors omitted
    → ∀(check : List Check → Bar)
    → Bar

During the stage of config processing, Bars are converted to a type called Plugin, which is a list of Tokens (in fact, List is the only recursive data type in Dhall). These tokens can be marshalled into Haskell, and then parsed back into a tree structure (DzenDhall.Data.Bar).

After that, dzen-dhall spawns some threads for each output source and processes the outputs as specified in the configuration.

Deduplication

This is a really tricky part: identical-by-definition sources or automata within the same scope are always treated as a single one, no matter how many times they appear in a Bar tree.

A simplest example that makes deduplication observable can be found here.

Deduplication was introduced to handle gracefully the situation where we have an automaton with a StateMap of multiple states, some of which contain the same sources and/or automata. Do we want to create these duplicating things for each possible state? Of course, not: this is like saying no to performance from the start. The chosen solution was to just remove these duplicates from runtime.

The reason why we have to deal with this problem at all (while normal reactive frameworks don't have to) is because all sources in a StateMap are being run at the same time in background (no matter in which state the automaton is), but the user is only able to observe the output corresponding to exactly one state. This may seem strange, but in fact this approach has its own benefits. For example, output is always available immediately after a state change. And the implementation is much simpler, because there is no need to kill output sources and launch them anew.

If you want "normal" behavior, it's not hard to define an automaton that does not contain sources in its StateMap at all, and define a single output source that queries the state of this automaton and switches between various outputs. Of course, you may ask, "what if I want to render complex markup that changes depending on the state?". The answer is that you just have to put your automaton back in a StateMap wherever you want it to appear, possibly duplicating it multiple times in various contexts - and at runtime it will be deduplicated!