#!/bin/bash

# generate-configs - Docstring parser for generating xmonad build configs with
#                    default settings for extensions
# Author: Alex Tarkovsky <alextarkovsky@gmail.com>
# Released into the public domain

# This script parses custom docstrings specifying build-time configuration data
# from xmonad extension source files, then inserts the data into copies of
# xmonad's Config.hs and xmonad.cabal files accordingly.
#
# Usage: generate-configs [ OPTIONS ] --main MAIN_DIR --contrib CONTRIB_DIR
#
# OPTIONS:
#   --active,  -a              Insert data in active mode (default: passive)
#   --contrib, -c CONTRIB_DIR  Path to contrib repository base directory
#   --help,    -h              Show help
#   --main,    -m MAIN_DIR     Path to main repository base directory
#   --output,  -o OUTPUT_DIR   Output directory (default: CONTRIB_DIR)
#
# Data parsed from the extension source files is inserted into Config.hs in
# either active or passive mode. The default is passive mode, in which the
# inserted data is commented out. The --active option inserts the data
# uncommented. Data inserted into xmonad.cabal is always inserted in active
# mode regardless of specified options.
#
# The docstring markup can be extended as needed. Currently the following tags
# are defined, shown with some examples:
#
# ~~~~~
#
# %cabalbuilddep
#
#     Cabal build dependency. Value is appended to the "build-depends" line in
#     xmonad.cabal and automatically prefixed with ", ".  NB: Don't embed
#     comments in this tag!
#
# -- %cabalbuilddep readline>=1.0
#
# %def
#
#     General definition. Value is appended to the end of Config.sh.
#
# -- %def commands :: [(String, X ())]
# -- %def commands = defaultCommands
#
# %import
#
#     Module needed by Config.sh to build the extension. Value is appended to
#     the end of the default import list in Config.sh and automatically
#     prefixed with "import ".
#
# -- %import XMonad.Layout.Accordion
# -- %import qualified XMonad.Actions.FlexibleManipulate as Flex
#
# %keybind
#
#     Tuple defining a key binding. Must be prefixed with ", ". Value is
#     inserted at the end of the "keys" list in Config.sh.
#
# -- %keybind , ((modMask, xK_d), date)
#
# %keybindlist
#
#     Same as %keybind, but instead of a key binding tuple the definition is a
#     list of key binding tuples (or a list comprehension producing them). This
#     list is concatenated to the "keys" list must begin with the "++" operator
#     rather than ", ".
#
# -- %keybindlist ++
# -- %keybindlist -- mod-[1..9] @@ Switch to workspace N
# -- %keybindlist -- mod-shift-[1..9] @@ Move client to workspace N
# -- %keybindlist -- mod-control-shift-[1..9] @@ Copy client to workspace N
# -- %keybindlist [((m .|. modMask, k), f i)
# -- %keybindlist     | (i, k) <- zip [0..fromIntegral (workspaces-1)] [xK_1 ..]
# -- %keybindlist     , (f, m) <- [(view, 0), (shift, shiftMask), (copy, shiftMask .|. controlMask)]]
#
# %layout
#
#     A layout. Must be prefixed with ", ". Value is inserted at the end of the
#     "defaultLayouts" list in Config.sh.
#
# -- %layout , accordion
#
# %mousebind
#
#     Tuple defining a mouse binding. Must be prefixed with ", ". Value is
#     inserted at the end of the "mouseBindings" list in Config.sh.
#
# -- %mousebind , ((modMask, button3), (\\w -> focus w >> Flex.mouseResizeWindow w))
#
# ~~~~~
#
# NB: '/' and '\' characters must be escaped with a '\' character!
#
# Tags may also contain comments, as illustrated in the %keybindlist examples
# above. Comments are a good place for special user instructions:
#
# -- %def -- comment out default logHook definition above if you uncomment this:
# -- %def logHook = dynamicLog

# Markup tag to search for in source files.
TAG_CABALBUILDDEP="%cabalbuilddep"
TAG_DEF="%def"
TAG_IMPORT="%import"
TAG_KEYBIND="%keybind"
TAG_KEYBINDLIST="%keybindlist"
TAG_LAYOUT="%layout"
TAG_MOUSEBIND="%mousebind"

# Insert markers to search for in Config.sh and xmonad.cabal. Values are
# extended sed regular expressions.
INS_MARKER_CABALBUILDDEP='^build-depends:.*'
INS_MARKER_IMPORT='-- % Extension-provided imports$'
INS_MARKER_LAYOUT='-- % Extension-provided layouts$'
INS_MARKER_KEYBIND='-- % Extension-provided key bindings$'
INS_MARKER_KEYBINDLIST='-- % Extension-provided key bindings lists$'
INS_MARKER_MOUSEBIND='-- % Extension-provided mouse bindings$'
INS_MARKER_DEF='-- % Extension-provided definitions$'

# Literal indentation strings. Values may contain escaped chars such as \t.
INS_INDENT_CABALBUILDDEP=""
INS_INDENT_DEF=""
INS_INDENT_IMPORT=""
INS_INDENT_KEYBIND="    "
INS_INDENT_KEYBINDLIST="    "
INS_INDENT_LAYOUT="                 "
INS_INDENT_MOUSEBIND="    "

# Prefix applied to inserted passive data after indent strings have been applied.
INS_PREFIX_DEF="-- "
INS_PREFIX_IMPORT="--import "
INS_PREFIX_KEYBIND="-- "
INS_PREFIX_KEYBINDLIST="-- "
INS_PREFIX_LAYOUT="-- "
INS_PREFIX_MOUSEBIND="-- "

# Prefix applied to inserted active data after indent strings have been applied.
ACTIVE_INS_PREFIX_CABALBUILDDEP=", "
ACTIVE_INS_PREFIX_DEF=""
ACTIVE_INS_PREFIX_IMPORT="import "
ACTIVE_INS_PREFIX_KEYBIND=""
ACTIVE_INS_PREFIX_KEYBINDLIST=""
ACTIVE_INS_PREFIX_LAYOUT=""
ACTIVE_INS_PREFIX_MOUSEBIND=""

# Don't touch these
opt_active=0
opt_contrib=""
opt_main=""
opt_output=""

generate_configs() {
    for extension_srcfile in $(ls --color=never -1 "${opt_contrib}"/*.hs | head -n -1 | sort -r) ; do
        for tag in $TAG_CABALBUILDDEP \
                   $TAG_DEF \
                   $TAG_IMPORT \
                   $TAG_KEYBIND \
                   $TAG_KEYBINDLIST \
                   $TAG_LAYOUT \
                   $TAG_MOUSEBIND ; do

            ifs="$IFS"
            IFS=$'\n'
            tags=( $(sed -n -r -e "s/^.*--\s*${tag}\s//p" "${extension_srcfile}") )
            IFS="${ifs}"

            case $tag in
                $TAG_CABALBUILDDEP) ins_indent=$INS_INDENT_CABALBUILDDEP
                                    ins_marker=$INS_MARKER_CABALBUILDDEP
                                    ins_prefix=$ACTIVE_INS_PREFIX_CABALBUILDDEP
                                    ;;
                $TAG_DEF)           ins_indent=$INS_INDENT_DEF
                                    ins_marker=$INS_MARKER_DEF
                                    ins_prefix=$INS_PREFIX_DEF
                                    ;;
                $TAG_IMPORT)        ins_indent=$INS_INDENT_IMPORT
                                    ins_marker=$INS_MARKER_IMPORT
                                    ins_prefix=$INS_PREFIX_IMPORT
                                    ;;
                $TAG_KEYBIND)       ins_indent=$INS_INDENT_KEYBIND
                                    ins_marker=$INS_MARKER_KEYBIND
                                    ins_prefix=$INS_PREFIX_KEYBIND
                                    ;;
                $TAG_KEYBINDLIST)   ins_indent=$INS_INDENT_KEYBINDLIST
                                    ins_marker=$INS_MARKER_KEYBINDLIST
                                    ins_prefix=$INS_PREFIX_KEYBINDLIST
                                    ;;
                $TAG_LAYOUT)        ins_indent=$INS_INDENT_LAYOUT
                                    ins_marker=$INS_MARKER_LAYOUT
                                    ins_prefix=$INS_PREFIX_LAYOUT
                                    ;;
                $TAG_MOUSEBIND)     ins_indent=$INS_INDENT_MOUSEBIND
                                    ins_marker=$INS_MARKER_MOUSEBIND
                                    ins_prefix=$INS_PREFIX_MOUSEBIND
                                    ;;
            esac

            # Insert in reverse so values will ultimately appear in correct order.
            for i in $( seq $(( ${#tags[*]} - 1 )) -1 0 ) ; do
                [ -z "${tags[i]}" ] && continue
                if [[ $tag == $TAG_CABALBUILDDEP ]] ; then
                    sed -i -r -e "s/${ins_marker}/\\0${ins_prefix}${tags[i]}/" "${CABAL_FILE}"
                else
                    sed -i -r -e "/${ins_marker}/{G;s/$/${ins_indent}${ins_prefix}${tags[i]}/;}" "${CONFIG_FILE}"
                fi
            done

            if [[ $tag != $TAG_CABALBUILDDEP && -n "${tags}" ]] ; then
                ins_group_comment="${ins_indent}--   For extension $(basename $extension_srcfile .hs):"
                sed -i -r -e "/${ins_marker}/{G;s/$/${ins_group_comment}/;}" "${CONFIG_FILE}"
            fi
        done
    done
}

parse_opts() {
    [[ -z "$1" ]] && show_usage 1

    while [[ $# > 0 ]] ; do
        case "$1" in
            --active|-a)  opt_active=1
                          shift ;;

            --contrib|-c) shift
                          if [[ -z "$1" || ! -d "$1" ]] ; then
                              echo "Error: Option --contrib requires a directory as argument. See: generate-configs -h"
                              exit 1
                          fi
                          opt_contrib="$1"
                          shift ;;

            --help|-h)    show_usage ;;

            --main|-m)    shift
                          if [[ -z "$1" || ! -d "$1" ]] ; then
                              echo "Error: Option --main requires a directory as argument. See: generate-configs -h"
                              exit 1
                          fi
                          opt_main="$1"
                          shift ;;

            --output|-o)  shift
                          if [[ -z "$1" || ! -d "$1" ]] ; then
                              echo "Error: Option --output requires a directory as argument. See: generate-configs -h"
                              exit 1
                          fi
                          opt_output="$1"
                          shift ;;

            -*)           echo "Error: Unknown option ${1}. See: generate-configs -h"
                          exit 1 ;;

            *)            show_usage 1 ;;
        esac
    done

    if [[ -z "$opt_main" ]] ; then
      echo "Error: Missing required option --main. See: generate-configs -h"
      exit 1
    fi

    if [[ -z "$opt_contrib" ]] ; then
      echo "Error: Missing required option --contrib. See: generate-configs -h"
      exit 1
    fi
}

show_usage() {
cat << EOF
Usage: generate-configs [ OPTIONS ] --main MAIN_DIR --contrib CONTRIB_DIR

OPTIONS:
  --active,  -a              Insert data in active mode (default: passive)
  --contrib, -c CONTRIB_DIR  Path to contrib repository base directory
  --help,    -h              Show help
  --main,    -m MAIN_DIR     Path to main repository base directory
  --output,  -o OUTPUT_DIR   Output directory (default: CONTRIB_DIR)
EOF
    exit ${1:-0}
}

parse_opts $*

[[ -z "$opt_output" ]] && opt_output="$opt_contrib"

CABAL_FILE="${opt_output}/xmonad.cabal"
CONFIG_FILE="${opt_output}/Config.hs"

cp -f "${opt_main}/xmonad.cabal" "${CABAL_FILE}"
cp -f "${opt_main}/Config.hs" "${CONFIG_FILE}"

if [[ $opt_active == 1 ]] ; then
    INS_PREFIX_DEF=$ACTIVE_INS_PREFIX_DEF
    INS_PREFIX_IMPORT=$ACTIVE_INS_PREFIX_IMPORT
    INS_PREFIX_KEYBIND=$ACTIVE_INS_PREFIX_KEYBIND
    INS_PREFIX_KEYBINDLIST=$ACTIVE_INS_PREFIX_KEYBINDLIST
    INS_PREFIX_LAYOUT=$ACTIVE_INS_PREFIX_LAYOUT
    INS_PREFIX_MOUSEBIND=$ACTIVE_INS_PREFIX_MOUSEBIND
fi

generate_configs
