#!/bin/bash

# bspwm_dynamic_desktops --- Dynamic desktops (work spaces) for BSPWM.
#
# Copyright (c) 2019-2020 Protesilaos Stavrou <info@protesilaos.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
## Commentary:
#
# The idea with dynamic desktops is to maintain a list of varying
# lengths with only the active items.  Whereas the default is to keep a
# fixed list of, say, ten desktops at all times.
#
# What this script does, in outline:
#
# - Switching to a non-existent desktop will create it dynamically.
# - Sending a node to a given desktop will create that desktop as well.
#   - There also exists a variant that follows the node to that desktop.
# - Empty desktops are removed.
#
# Furthermore, there exists a toggle for multi-head setups (typically
# dual-monitor) which allows us to bind desktops to monitors, while
# retaining the aforementioned patterns of behaviour.  In practice this
# means that the relevant commands become agnostic to the focused
# monitor so that, e.g., switching to desktop 1 will always take you to
# desktop 1 which belongs to monitor A.
#
# This script's commands are meant to be implemented in SXHKD (or
# equivalent).  Furthermore, the desktops need to be set to one per
# monitor in bspwmrc (empty desktops are removed upon invoking one of
# this script's commands).
#
# `bspwm_dynamic_desktops' is part of my dotfiles (which also offer a
# complete implementation): https://gitlab.com/protesilaos/dotfiles



# See the --options below for the available arguments for $1.  $2 must
# be a number from 0 to 9.
[ "$#" -eq 2 ] || { echo 'Must be run with two arguments.'; exit 1; }

# Capture the arguments.  We need to run tests against them.  Renaming
# them makes things easier to read.
option="${*:1:1}"
target_desktop="${*:2:1}"

case "$target_desktop" in
    [!0-9])
        echo "The second argument must be a valid number"
        exit 1
        ;;
esac

# The `dynamic_but_dedicated' switch concerns multi-head setups
# (typically dual-monitor).  It allows us to specify a range of numbered
# desktops that we would want to always place on dedicated monitors.
# This means that while we retain the functionality of dynamically
# creating/removing desktops, we also bind certain desktops to monitors.
#
# NOTE: it only applies to the `--ns*' options (see the last part of the
# script).  As such, you can still send a node to any
# dynamically-created numbered desktop on the next monitor, by using the
# `--nm*' options.
if [ "$(bspc query -M | wc -l)" -gt 1 ]; then
    # Switch to `false' to restore the monitor-independent behaviour.
    dynamic_but_dedicated='true'
fi

# Here we specify the range of desktops that should always appear on the
# monitor named `LVDS-1'.  Everything else appears on `VGA-1'.  You can
# find the names of your monitors with `bspc query -M --names`.
#
# NOTE: if you use more that two monitors, you need to create another
# range similar to the one for LVDS-1 and adapt things accordingly.
if [ "$dynamic_but_dedicated" = 'true' ]; then
    dedicated_desktops='true'
    case "$target_desktop" in
        [1-5]) dedicated_monitor="LVDS-1" ;;
        *)     dedicated_monitor="VGA-1"  ;;
    esac
fi

# Command for querying BSPWM desktops.  Used further below.
_query_desktops() {
    bspc query -D -d "$@"
}

# The core functionality of this script.  Behaviour changes based on
# whether this is a desktop or a node (each action is mapped to
# different key chords in sxhkd).
#
# If the target desktop does not exist, it is created on the spot.
_desk_or_node() {
    _dynamic_desktops() {
        # Here we make sure to clean up the arguments passed to this
        # function.  We can operate on them with greater flexibility.
        local monitor action
        monitor="${*:1:1}"
        action="${*:2}"

        # Leave the unquoted in tact!  Else the commands will not run.
        if ! _query_desktops "$target_desktop" > /dev/null; then
            bspc monitor "$monitor" -a "$target_desktop" && $action
        else
            # BACK-AND-FORTH behaviour: inputting the number of the
            # focused desktop switches to the last one.
            #
            # If you do not like this, just replace the whole if/fi part
            # below with $action (and do not quote it).
            if [ "$(_query_desktops --names)" -eq "$target_desktop" ]; then
                bspc desktop -f last
            else
                $action
            fi
        fi
    }

    # Reorder desktops on the target monitor (the target is the first
    # argument passed to this function).  The default is the focused
    # one.
    _desk_order() {
        while read -r line; do
            printf "%s\\n" "$line"
        done < <(bspc query -D -m "${1:-focused}" --names) | sort -g | paste -d ' ' -s
    }

    # Determines whether the behaviour of this function concerns nodes
    # or desktops.  The "monitor" is just a filter for nodes/desktops
    # (hence the `shift').  It concerns multihead setups.
    case "$1" in
        node|desktop)
            _dynamic_desktops 'focused.focused' bspc "$@"
            # Do not quote the following `$(…)` we want term splitting
            # here.  The next comment silences the `shellcheck' utility.
            #
            # shellcheck disable=SC2046
            bspc monitor -o $(eval _desk_order)
            ;;
        monitor)
            shift
            if [ -n "$dedicated_desktops" ]; then
                _dynamic_desktops "$dedicated_monitor" bspc "$@"
                # Do not quote the following `$(…)` we want term splitting
                # here.  The next comment silences the `shellcheck' utility.
                #
                # shellcheck disable=SC2046
                bspc monitor "$dedicated_monitor" -o $(eval _desk_order "$dedicated_monitor")
            else
                _dynamic_desktops next bspc "$@"
                # Do not quote the following `$(…)` we want term splitting
                # here.  The next comment silences the `shellcheck' utility.
                #
                # shellcheck disable=SC2046
                bspc monitor next -o $(eval _desk_order next)
            fi
            ;;
    esac
}

# Invoke the above command, passing to it arguments that change its
# behaviour (desktop or node).
#
# Mnemonics for shorter options:
# n* == node
# d* == desktop
case "$option" in
    --na|--send-all-to-desktop)
        _desk_or_node node 'any.local' -d "${target_desktop}"
        ;;
    --ns|--send-focused-to-desktop)
        if [ -n "$dedicated_desktops" ]; then
            _desk_or_node monitor node focused -d "${target_desktop}"
        else
            _desk_or_node node focused -d "${target_desktop}"
        fi
        ;;
    --nsf|--send-focused-to-desktop-and-follow)
        if [ -n "$dedicated_desktops" ]; then
            _desk_or_node monitor node focused -d "${target_desktop}" --follow
        else
            _desk_or_node node focused -d "${target_desktop}" --follow
        fi
        ;;
    --nm|--send-focused-to-next-monitor)
        _desk_or_node monitor node focused -d "${target_desktop}"
        ;;
    --nmf|--send-focused-to-next-monitor-and-follow)
        _desk_or_node monitor node focused -d "${target_desktop}" --follow
        ;;
    --da|--activate-desktop-and-place-receptacle)
        _desk_or_node monitor node @"$target_desktop":/ -i && bspc desktop -a "${target_desktop}"
        ;;
    --df|--focus-desktop)
        if [ -n "$dedicated_desktops" ]; then
            _desk_or_node monitor desktop -f "${target_desktop}"
        else
            _desk_or_node desktop -f "${target_desktop}"
        fi
        ;;
    *)
        echo "< $option > is not a valid option."
        exit 1
        ;;
esac

# Remove empty desktops.  This works for multiple monitors, as well.
# This will NOT REMOVE empty desktops that contain only receptacles
# (applies to the --activate-desktop option above).
for i in $(_query_desktops '.!focused.!occupied' --names); do
    bspc desktop "$i" -r
done
