#!/bin/sh
#
# chdt -- Change desktop in a 2-D plane, optionally bringing the active window
#
# Copyright (C) 2018  Patrick "P. J." McDermott
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

set -eu

# Default values, overridden in ~/.chdtrc
rows=2
cols=6
orientation='horizontal'

debug=0

err()
{
	local fmt="${1}"
	shift 1

	printf "Error: ${fmt}\n" "${@}" 1>&2
	return 0
}

dbg()
{
	local fmt="${1}"
	shift 1

	case "${debug}" in '1')
		printf "DEBUG: ${fmt}\n" "${@}" 1>&2
	esac

	return 0
}

get_desktop()
{
	local dt=

	dt=$(wmctrl -d | sed -n '/^[0-9][0-9]*  *\* /{ s/  *\* .*$//; p; q; };')
	case "${dt}" in
		*[!0-9]* | '')
			err 'Failed to get current desktop number'
			return 1
			;;
	esac

	dbg 'Old desktop: %d' ${dt}

	printf '%d' ${dt}
	return 0
}

inc_desktop()
{
	local dt=${1}
	local dx=${2}
	local dy=${3}

	case "${orientation}" in
		[Hh]*)
			x=$((${dt} % ${cols}))
			y=$((${dt} / ${cols}))
			dbg 'Old coordinates: (%d, %d)' ${x} ${y}
			x=$(((${x} + ${cols} + ${dx}) % ${cols}))
			y=$(((${y} + ${rows} + ${dy}) % ${rows}))
			dbg 'New coordinates: (%d, %d)' ${x} ${y}
			dt=$((${y} * ${cols} + ${x}))
			;;
		[Vv]*)
			x=$((${dt} / ${rows}))
			y=$((${dt} % ${rows}))
			x=$(((${x} + ${dx}) % ${cols}))
			y=$(((${y} + ${dy}) % ${rows}))
			dt=$((${x} * ${rows} + ${y}))
			;;
	esac

	printf '%d' ${dt}

	return 0
}

set_desktop()
{
	local dt=${1}
	shift 1

	dbg 'New desktop: %d' ${dt}

	wmctrl -s ${dt}

	return 0
}

move_window()
{
	local dt=${1}
	shift 1

	dbg 'New desktop: %d' ${dt}

	wmctrl -r ':ACTIVE:' -t ${dt}
	wmctrl -s ${dt}

	return 0
}

usage()
{
	printf 'Usage: %s [<options>] <direction>\n' "${0}"
	printf '\n'
	printf 'Where <direction> is any word beginning with (in any case):\n'
	printf '  * "U" for up\n'
	printf '  * "D" for down\n'
	printf '  * "L" for left\n'
	printf '  * "R" for right\n'
	printf '\n'
	printf 'Options:\n'
	printf '  -w  Move the active window\n'
	printf '  -d  Enable debugging output\n'

	return 0
}

main()
{
	local move_win=
	local opt=
	local dir=
	local dx=
	local dy=
	local cur_dt=
	local new_dt=

	move_win=0

	while getopts 'wd' opt "${@}"; do
		case "${opt}" in
			'w') move_win=1;;
			'd') debug=1;;
			'?')
				usage 1>&2
				return 1
				;;
		esac
	done
	shift $((${OPTIND} - 1))

	if [ ${#} -ne 1 ]; then
		usage 1>&2
		return 1
	fi

	dir="${1}"
	shift 1

	case "${dir}" in
		[Uu]*) dx=0  dy=-1;;
		[Dd]*) dx=0  dy=1 ;;
		[Ll]*) dx=-1 dy=0 ;;
		[Rr]*) dx=1  dy=0 ;;
		*)
			usage 1>&2
			return 1
			;;
	esac

	# POSIX on the dot utility:
	# "If no readable file is found, a non-interactive shell shall abort"
	# So to survive an ENOENT or other error, we need this eval/cat command.
	eval "$(cat ~/.chdtrc 2>/dev/null || :)"

	cur_dt=$(get_desktop) || return 1
	new_dt=$(inc_desktop ${cur_dt} ${dx} ${dy})
	case ${move_win} in
		1) move_window ${new_dt};;
		0) set_desktop ${new_dt};;
	esac

	return 0
}

main "${@}"