#!/bin/ksh
# cloud wrapper script for Subversion/Mercurial
# Author: Perette Barella
# Copyright 2018 - 2019 Devious Fish.  All rights reserved.
VERSION='$Id: cloud 50 2019-03-02 19:03:26Z perette $'

readonly arg0=$(basename "$0")
MODERN_GETOPTS=false
[[ $(getopts '[-][12:abc]' flag --abc; print -- 0$flag) == "012" ]] &&
	MODERN_GETOPTS=true
EXIT_STATUS=$'0 on success, non-0 on error'

# Open file with window manager
# Author: Perette Barella
# Copyright 2018 Devious Fish.  All rights reserved.
# $Id: launch_document 40 2018-10-01 19:56:46Z perette $

# Korn shell find a utility to implement something
# Author: Perette Barella
# Copyright 2018 Devious Fish.  All rights reserved.
# $Id: find_implementation 35 2018-09-13 20:31:44Z perette $

function find_implementation {
	typeset impl utility
	for impl
	do
		utility="${impl%%[ 	]*}"
		if whence -q "$utility"
		then
			print -- "$impl"
			return 0
		fi
	done
	return 1
}


# Parameters: List of documents or URLs to open.
# Returns: 0 on success, non-0 on error.
function launch_document {
	typeset opener document
	if [[ $(uname -s) == "Darwin" ]]
	then
		opener="open %s"
	else
		# Redirects prevent application X spooge all over the terminal
		if ! opener=$(find_implementation \
			"gnome-open %s >/dev/null 2>&1" \
			"xdg-open %s >/dev/null 2>&1" \
			"gvfs-open %s >/dev/null 2>&1")
		then
			print "$arg0: No file opening utility found for this platform."
			return 1
		fi
	fi
	integer status=0 
	for document
	do
		typeset cmd=$(printf "$opener" \'"$document"\')
		eval "$cmd"
		typeset result=$?
		# If the command fails and involves a redirect, perform it
		# again without the redirect to show the error.
		if (( result != 0 )) && [[ $opener == *">"* ]]
		then
			eval "${cmd%%">"*}"
		fi
		
	done
	return $status
}

# Search for text
# Author: Perette Barella
# Copyright 2018 Devious Fish.  All rights reserved.
# $Id: search_for_text 40 2018-10-01 19:56:46Z perette $


# Check if 0 or more patterns match a line of text.
# Comparison is case insensitive.
# $1 - The text to match against.
# $2.. - The patterns.  May be shell patterns.
# Return: 0 if all patterns match, non-0 otherwise.
function text_search_line_matches_all {
	typeset -l pattern line="$1"
	shift
	for pattern
	do
		eval "[[ \$line != *\${pattern}* ]]" && return 1
	done
	return 0
}


# Search input stream for matching text.  Matches are sent to stdout.
# Comparison is case insensitive
# $1 - Method to search by: 'line' or 'paragraph'
# $2 - Paragraph separator.  If "", blank/all whitespace lines.
# $3.. - The patterns.
function search_for_text {
	typeset -r mode="$1" divider="$2"
	shift 2
	if [[ "$mode" != line && "$mode" != paragraph ]]
	then
		print -- "cloud_extract: Assertion: $mode: Must be 'line' or 'paragraph'." 1>&2
		exit 1
	fi

	integer -r paragraph_max=1024
	integer lines=0
	typeset match=false completed=false out
	typeset -a paragraph
	while ! $completed
	do
		IFS="" read -r line || completed=true
		if [[ "$mode" = "paragraph" ]]
		then
			if $completed ||
			   [[ $divider == "" && $line == *([ \t]) ]] ||
			   eval "[[ \"\$divider\" != \"\" && \"\$line\" == \$divider ]]"
			then
				if (( lines > 0 )) &&
				   text_search_line_matches_all "${paragraph[*]}" "$@"
				then
					for out in "${paragraph[@]}"
					do
						print -r -- "$out"
					done
					match=true
				fi
				# Reset the paragraph buffer
				unset paragraph
				typeset -a paragraph
				lines=0
			fi
			if (( lines < paragraph_max ))
			then
				paragraph[lines++]="$line"
			fi
		elif ! $completed && text_search_line_matches_all "$line" "$@"
		then
			match=true
			print -r -- "$line"
		fi
	done
	$match && return 0
	return 255
}



function wait_for_user {
	typeset junk
	[[ -n "$*" ]] && print -- "$*" 1>&2
	read -p "Press [enter] to continue: " junk
}


# Return the '-a progname' portion of getopts string.
function getopts_options {
	if $MODERN_GETOPTS
	then
		(( $# == 0 )) && print -- -a "$arg0"
		(( $# == 1 )) && print -- "-a '$arg0 $1'"
	fi
}

# Assemble getopts string
# @param $1 - text and options
# @param $2 - Usage/parameters
# @return Completed parameter string, with common details added.
# If modern getopts is unavailable, a posix-style getopts string.
function get_options {
	typeset opts="" line
	$MODERN_GETOPTS && print -- "
[-1?$VERSION]$1
[+EXIT_STATUS?$EXIT_STATUS]"$'
[+ENVIRONMENT]
  {
    [+DCLOUD_BACKEND?Set to \bhg\b for Mercurial, or \bsvn\b for Subversion.]
    [+DCLOUD_MERGE?Preferred merge utility in case manual file merging is necessary.]
    [+EDITOR?Selects the text editor, unless \aVISUAL\a is set.]
    [+VISUAL?Selects a text editor.  If neither \aVISUAL\a nor \aEDITOR\a are set, defaults to \bvi\b(1), which is often really \bvim\b these days.]
  }
[+FILES?Files with a \v.cpt\v extension are encrypted.]

'"$2"$'

[-author?Perette Barella <perette@deviousfish.com>]' && return 0
	print -- "$1" | while read line
	do
		if [[ "${line:0:1}" == "[" && "${line:2:1}" == ":" ]]
		then
			opts="$opts${line:1:1}"
			[[ "$line" == *][:#]\[* ]] && opts="$opts:"
		fi
	done
	print -- "$opts"
	return 0
}


######################################################################
# None backend
######################################################################
function resolve_conflicts_none {
	return 0
}

function cloud_check_none {
	return 0
}

function cloud_push_none {
	return 0
}

function cloud_sync_none {
	return 0
}

function cloud_pull_none {
	return 0
}

function cloud_add_none {
	return 0
}

######################################################################
# Mercurial backend
######################################################################

function cloud_check_hg {
	if [[ ! -d ".hg" ]]
	then
		print "$PWD/.hg: Repository not found." 1>&2
		return 1
	fi
	if [[ ! -e ".hg/hgrc" ]]
	then
		cat > .hg/hgrc <<# EOF
			[paths]
			# default = ssh://somehost.whatever/repository/location
		EOF
#
	fi
	if ! grep "^default" .hg/hgrc
	then
		print "$PWD/.hg/hgrc: Default upstream repository not configured." 1>&2
		return 1
	fi
	return 0
}

# Logic in the function is dodgy: race condition between asynchronous repository changes and resolving conflicts
function cloud_push_hg {
	typeset -r file="$1" message="${2:-Modified $file}"
	typeset conflicts

	hg commit -m "$message" && hg push
}

function cloud_sync_hg {
	typeset -r message="$1"
	typeset changes problems cruft
	hg pull .
	hg update -t "$DCLOUD_MERGE"

	changes=$(hg status . | grep -c -v '^\?')
	if (( changes > 0 ))
	then
		problems=$(hg status --deleted)
		if [[ -z "$problems" ]]
		then
			hg commit -m "${message}" || return $?
			hg push .
			hg update -t "$DCLOUD_MERGE"
		else
			print -- "$arg0: Files missing from cloud area." 1>&2
			hg status | grep '^!'
			return 1
		fi
	fi
	cruft=$(hg status | grep -c '^\?')
	if (( cruft > 0 ))
	then
		print -- "$arg0: Extra files present in cloud area." 1>&2
		hg status | grep '^\?' | sed $'s/^/\t/'
		return 2
	fi
	return 0
}


function cloud_pull_hg {
	typeset file="$1"
	if (( $# > 0 )) && [[ -f "$file" ]]
	then
		hg pull && hg update -t "$DCLOUD_MERGE"
	else
		hg pull && hg update -t "$DCLOUD_MERGE"
	fi
}

function cloud_add_hg {
	typeset -r file="$1" message="$2"
	svn add "$file" && cloud_push_hg "$file" "$message"
}

######################################################################
# SVN backend
######################################################################
function resolve_conflicts_svn {
	typeset file="$1"
	while (( $(svn status "$file" | grep -c '^C') > 0 ))
	do
		$DCLOUD_MERGE "$file" >/dev/null 2>&1 &
		wait_for_user "Launched merge utility; please resolve merge conflicts."
		svn resolve --accept 'working' "$file" 1>&2
		svn update -q --accept 'postpone' "$file"
	done
}

function cloud_check_svn {
	if [[ ! -d ".svn" ]]
	then
		print "$PWD/.svn: Repository not found." 1>&2
		return 1
	fi
}


# Logic in the function is dodgy: race condition between asynchronous repository changes and resolving conflicts
function cloud_push_svn {
	typeset -r file="$1" message="$2"

	svn update -q --accept 'postpone' "$file"
	resolve_conflicts_svn "$file"
	svn commit -m "$message" "$file"
}

function cloud_sync_svn {
	typeset -r message="$1"
	typeset changes conflicts cruft
	svn update -q --accept 'postpone'

	changes=$(svn status | grep -c -v '^\?')
	if (( changes > 0 ))
	then
		conflicts=$(svn status | grep -c '^C')
		if (( conflicts == 0 ))
		then
			svn commit -m "${message}" || return $?
			svn update -q --accept 'postpone'
		else
			print -- "$arg0: Cloud conflicts require resolution." 1>&2
			svn status | grep '^C'
			return 1
		fi
	fi
	cruft=$(svn status | grep -c '^\?')
	if [ $cruft -gt 0 ]
	then
		print -- "$arg0: Extra files present in cloud area." 1>&2
		svn status | grep '^\?' | sed $'s/^/\t/'
		return 2
	fi
	return 0
}


function cloud_pull_svn {
	typeset -r file="$1"
	if (( $# > 0 )) && [[ -f "$file" ]]
	then
		svn update -q --accept 'postpone' "$file"
		resolve_conflicts_svn "$file"
	else
		svn update --accept 'postpone' -q
	fi
}

function cloud_add_svn {
	typeset -r file="$1" message="$2"
	svn add "$file" && cloud_push_svn "$file" "$message"
}

######################################################################
# API functions, which simply call the appropriate back end
######################################################################

# Check if the backend's repository looks sane.
function cloud_check {
	eval "cloud_check_${DCLOUD_BACKEND}"
}

# Push all pending changes into the back-end repository.
# @param $1 - optional message (always provided to handlers)
function cloud_sync {
	typeset -r message="${1:-Unexplained changes to files.}"
	eval "cloud_sync_${DCLOUD_BACKEND} \"\$message\""
}

# Push a specific file into the repository.
# @param $1 - filename
# @param $2 - optional message (always provided to handlers)
function cloud_push {
	typeset -r file="$1" message="${2:-Modified $file}"
	eval "cloud_push_${DCLOUD_BACKEND} \"\$file\" \"\$message\""
}

# Update a specific file to the latest in the repository.
# @param $1 - optional filename.  If omitted, update everything.
function cloud_pull {
	eval "cloud_pull_${DCLOUD_BACKEND} \"\$@\""
}


# Add a file to the repository
# @param $1 - filename
# @param $2 - Optional message (always provided to handlers)
function cloud_add {
	typeset -r file="$1" message="${2:-Created $file}"
	eval "cloud_add_${DCLOUD_BACKEND} \"\$file\" \"\$message\""
}



######################################################################
# Front-end functions
######################################################################

function cloud_publish {
	whence -q dcloud-post-commit &&
		dcloud-post-commit --local
}


# Determine what file is to be handled.
# Options:
#   -f: File is to be found.
#   -c: File is to be created.
# Parameters:
#   1 - the file name
# Returns: 0 on success, non-0 on error.
# Note: If we ever need to handle multiple files, create a locate_files
# wrapper that passes files one at a time for processing.
function locate_file {
	OPTIND=0
	typeset option
	typeset find=false create=false
	typeset -r options=$(get_options '
[c:create?]
[f:find?]
')
	
	while getopts "$options" option
	do
		case "$option" in
		    f)	find=true ;;
		    c)  create=true ;;
		esac
	done

	# Validate the options/parameters.
	shift $((OPTIND - 1))
	if $create && $find
	then
		print -- "$arg0 edit: -c (create) and -f (find) incompatible." 1>&2
		return 1
	fi
	if (( $# == 0)) || [[ "$1" == "" ]]
	then
		print -- "$arg0: File must be specified." 1>&2
		return 1
	fi
	if (( $# > 1 ))
	then
		print -- "$arg0: Must specify single file." 1>&2
		return 1
	fi
	typeset -r document="$1"
	typeset file=""

	# If finding, search out the file.  Refresh as necessary.
	if $find
	then
		cloud_pull
		if [[ -f "$DCLOUD/$document" ]]
		then
			file="$DCLOUD/$document"
		else
			typeset -a files
			files=( $DCLOUD/**/$document )
			(( ${#files[@]} == 0 )) &&
				 print -- "$file: File not found." 1>&2 &&
				 return 1
			(( ${#files[@]} > 1 )) &&
				 print -- "$arg0: Multiple documents match." 1>&2 &&
				 return 1
			file="${files[0]}"
		fi
	elif [ "${document:0:1}" = "/" ]
	then
		file="$document"
		cloud_pull "$file"
	else
		file="$DCLOUD/$document"
		cloud_pull "$file"
	fi

	# Validate what we found.
	if [[ -e "$file" && ! -f "$file" ]]
	then
		print -- "$document: Not a file." 1>&2
		return 1
	elif [[ "$create" == "false" && ! -f "$file" ]]
	then
		print -- "$document: Does not exist." 1>&2
		return 1
	fi

	# If this is an encrypted file, make sure utilities are present.
	if [[ $file == *.cpt ]] && ! whence -q ccrypt
	then
		print "$arg0: ccrypt require for encryption.  Please install." 1>&2
		return 4
	fi

	print -- "$file"
	return 0
}



function cloud_edit {
	typeset option file_opts=""
	typeset create=false publish=true text=false encrypted=false
	typeset -r goo=$(getopts_options edit) opts=$(get_options $'
[+SYNOPSIS?\f?\f - edit cloud document]
[+DESCRIPTION?\b\f?\f\b edits a cloud document.  If the repository
is available, the document is updated to the latest version prior to editing.
Text and Markdown documents are edited in the terminal.  For other documents,
system document-type preferences are used to launch an appropriate utility.]
[+?If the file has a \v.cpt\v extension, it is encrypted.  The file is
decrypted, and the resulting file edited, reencrypted, and removed.]
[c:create?Create file]
[f:find?Find file]
[s:suppress-publication?Do not invoke publisher after editing]
[t:text?Treat file as a text file, despite extension.]' 'document')
	while eval getopts $goo '"$opts"' option
	do
		case "$option" in
		    c)
			create=true
			file_opts="$file_opts -$option" ;;
		    f)
			file_opts="$file_opts -$option" ;;
		    s)	publish=false ;;
		    t)	text=true ;;
		esac
	done
	shift $((OPTIND - 1))

	typeset -r document="$1"
	typeset file
	file=$(locate_file $file_opts "$@") || return $?
	[[ $file == *.cpt ]] && encrypted=true
	typeset old_checksum="" edit_file="$file" old_umask="$(umask)"
	if $encrypted
	then
		if [[ $file != *.cpt ]]
		then
			print "$file: Must have .cpt extension for encryption." 1>&2
			return 1
		fi
		umask 0077
		edit_file="$(dirname "$file")/$(basename "$file" .cpt)"
		if [[ -f "$edit_file" ]]
		then
			print "$edit_file: Unencrypted file already exists." 1>&2
			return 1
		fi
		trap "rm -f \"$edit_file\"" EXIT
		if [[ -f "$file" ]] && ! ccrypt -c "$file" > "$edit_file"
		then
			return 1
		fi
	fi
	[[ -f "$file" ]] && old_checksum=$(cksum < "$edit_file")
	if $text || [[ $edit_file == *.txt || $edit_file == *.md ]]
	then
		(cd "$PRIOR_DIR" && ${VISUAL:-${EDITOR:-vi}} "$edit_file")
	elif [[ $edit_file == *.* ]]
	then
		(cd "$PRIOR_DIR" && launch_document "$edit_file" >/dev/null 2>&1)
		wait_for_user "$file: not a text file.  Editor launched." 1>&2
	else
		(cd "$PRIOR_DIR" && ${VISUAL:-${EDITOR:-vi}} "$edit_file")
	fi

	if [[ -f "$edit_file" ]]
	then
		typeset new_checksum=$(cksum < "$edit_file")
		if [[ "$old_checksum" != "$new_checksum" ]]
		then
			if $encrypted
			then
				while rm -f "$file"
				do
					ccrypt -e -t "$edit_file" && break
				done
			fi
			$encrypted && rm -f "$edit_file"
			if $create
			then
				cloud_add "$file" "Created $document"
			else
				cloud_push "$file" "Revised $document"
			fi
			$publish && cloud_publish >/dev/null 2>&1 &
		fi
	fi
	umask "$old_umask"
}

function cloud_cat {
	typeset option file_opts=""
	typeset -r goo=$(getopts_options cat) opts=$(get_options $'
[+SYNOPSIS?\f?\f - concatenate cloud documents to standard output]
[+DESCRIPTION?\b\f?\f\b concatenates documents, decrypting \v.cpt\v
documents as it goes.  When possible, documents are updated to the latest
version prior to output.]
[f:find?Find file]' 'document')
	while eval getopts $goo '"$opts"' option
	do
		case "$option" in
		    f)	file_opts="$file_opts -$option" ;;
		esac
	done
	shift $((OPTIND - 1))

	typeset document file status=0
	for document in "$@"
	do
		if file=$(locate_file $file_opts "$document")
		then
			if [[ $file == *.cpt ]]
			then
				ccrypt -c "$file" || status=$?
			else
				cat "$file" || status=$?
			fi
		else
			status=$?
		fi
	done
	return $status
}


function cloud_expand_name {
	typeset option file_opts=""
	typeset -r goo=$(getopts_options expand) opts=$(get_options $'
[+SYNOPSIS?\f?\f - expand filenames, optionally locating documents]
[+DESCRIPTION?\b\f?\f\b expands the names of the files provided.
When possible, documents are updated to the latest version prior to output.]
[f:find?Find file]
[1:single?Reject multiple files with an error.]
[100:no-encrypted?Reject encrypted filenames with an error.]' 'document')
	typeset allow_encrypted=true single=false
	while eval getopts $goo '"$opts"' option
	do
		case "$option" in
		    f)
			file_opts="$file_opts -$option" ;;
		    1)
			single=true ;;
		    100)
			allow_encrypted=false ;;
		esac
	done
	shift $((OPTIND - 1))

	if [[ $single == true ]] && (( $# > 1 ))
	then
		print -- "$arg0: Must specify single file." 1>&2
		return 1
	fi

	typeset document file status=0
	if [[ $allow_encrypted == false ]]
	then
		for document in "$@"
		do
			if [[ $document == *.cpt ]]
			then
				print -- "$document: Must not be encryped." 1>&2
				status=47
			fi
		done
	fi
	(( status != 0 )) && return $status

	typeset -a file_list
	for document in "$@"
	do
		if file=$(locate_file $file_opts "$document")
		then
			file_list[${#file_list[@]}]="$file"
		else
			status=$?
		fi
	done
	(( status != 0 )) && return $status
	for document in "${file_list[@]}"
	do
		print -- "$document"
	done
	return 0
}


function cloud_extract {
	typeset option file_opts="" mode=line divider=""
	EXIT_STATUS="0 if matches were found, 1 on error, 2 if no matches occurred."
	typeset -r goo=$(getopts_options extract) opts=$(get_options $'
[+SYNOPSIS?\f?\f - extract portions of a cloud document]
[+DESCRIPTION?\b\f?\f\b displays lines or paragraphs of a
document that contain specified search terms.  If multiple terms are
specified, all terms must be present to match.  If possible, the document is
updated before search.]
[d:divider?Paragraph divider.  If unspecified, blank lines are separators.]:[pattern]
[f:find?Find file]
[p:paragraph?Match paragraphs instead of lines.]' 'document search-text ...')
	while eval getopts $goo '"$opts"' option
	do
		case "$option" in
		    p)  mode=paragraph;;
		    d)  divider="$OPTARG" ;;
		    f)	file_opts="$file_opts -$option" ;;
		esac
	done
	shift $((OPTIND - 1))
	readonly mode divider

	typeset document="$1" file
	file=$(locate_file $file_opts "$document") || return 1
	shift
	if (( $# == 0 ))
	then
		print -- "$arg0 extract: 1 or more patterns must be specified."
		return 1
	fi
	set -o pipefail
	if [[ $file == *.cpt ]]
	then
		( ccrypt -c "$file" || return 1 )
	else
		( cat "$file" || return 1 )
	fi | ( search_for_text "$mode" "$divider" "$@" || return 2 )
	return $?
}

function cloud_sync_front_end {
	typeset message file_opts
	typeset -r goo=$(getopts_options sync) opts=$(get_options $'
[+SYNOPSIS?\f?\f - synchronize cloud documents]
[+DESCRIPTION?\b\f?\f\b synchronizes cloud documents,
retrieving updates from and/or pushing local updates to the repository. ]
[m:message?An explanation for any changes sent to the repository.]:[explanation]' '')
	while eval getopts $goo '"$opts"' option
	do
		case "$option" in
		    f)	file_opts="$file_opts -$option" ;;
		    m)	message="$OPTARG" ;;
		esac
	done
	shift $((OPTIND - 1))

	# Repository setup is hard, so just update everything if asked
	# to update more than one file.
	if (( $# != 1 ))
	then
		cloud_sync "$message" 
	else
		cloud_pull 
	fi
}


function usage {
	typeset goo="$1" opts="$2"
	OPTIND=0
	eval getopts $goo \"\$opts\" option "--help"
	exit 1
}



DCLOUD=${DCLOUD:-$HOME/Documents/Cloud}

PRIOR_DIR="$PWD"
cd "$DCLOUD" || exit 1

readonly goo=$(getopts_options)
readonly opts=$(get_options $'
[+SYNOPSIS?\f?\f - read, edit, and manipulate cloud documents]
[+DESCRIPTION?\b\f?\f\b is a wrapper for a Subversion or Mercurial repository,
allowing it to be used as a rudimentary cloud.  \b\f?\f\b automatically
refreshes documents before use, and when editing automatically pushes the
changes back with a generated commit comment.]
[+?Encrypted files may be managed by assigning them a \v.cpt\v extension.
These files are decrypted via pipelines when read; there are no intermediary
files.  When edited, they are decrypted to a temporary file
which is edited,
re-encrypted, and removed.  Crypto is done using
\bccrypt\b\(1).]
[+?View help for \b\f?\f\b subcommands using \b\f?\f\b \asubcommand\a
\b--help\b or \b\f?\f\b \asubcommand\a \b--man\b.]
[h:show-help?Show help.  Also --help.]
[+SEE ALSO?\bccrypt\b(1), \bnow\b(1), \bpass\b(1), \bpn\b(1)]' \
$'cat [-f] file ...
edit [-c|-f] [-t] file
extract [-d pattern] [-f] [-p] file
sync [message]')
while eval getopts $goo '"$opts"' option
do
	case "$option" in
	    h)
		usage "$goo" "$opts"
		;;
	esac
done
shift $((OPTIND - 1))


if [ $# -lt 1 ]
then
	print -- "$arg0: No action specified." 1>&2
	usage "$goo" "$opts"
fi

# Validate environment variables
if [[ -z "$DCLOUD_BACKEND" ]]
then
	if [[ -d .svn && -d .hg ]]
	then
		print -- "$arg0: Ambiguous backend, set DCLOUD_BACKEND." 1>&2
		exit 1
	fi
	if [[ -d .svn ]]
	then
		DCLOUD_BACKEND=svn
	elif [[ -d .hg ]]
	then
		DCLOUD_BACKEND=hg
	else
		print -- "$arg0: Repository connection is not set up." 1>&2
		exit 1
	fi
fi

if [[ "$DCLOUD_BACKEND" != @(svn|hg|none) ]]
then
	print "$arg0: Unknown backend '$DCLOUD_BACKEND'.  Check \$DCLOUD_BACKEND." 1>&2
	exit 127
fi

if [[ -n "$DCLOUD_MERGE" ]]
then
	if ! whence "$DCLOUD_MERGE" >/dev/null
	then
		print "$arg0: Merge utility '$DCLOUD_MERGE' not found.  Check \$DCLOUD_MERGE." 1>&2
		exit 127
	fi
else
	DCLOUD_MERGE="$(find_implementation meld diffmerge kompare diffuse kdiff3 xxdiff tkdiff)" ||
		DCLOUD_MERGE="wait_for_user Please merge the files: "
fi


# Let backend perform any sanity checking before we get started.
cloud_check || exit $?


readonly action="$1"
shift
case "$action" in
	sync)		cloud_sync_front_end "$@" ;;
	publish)	cloud_sync
			cloud_publish ;;
	edit)		cloud_edit "$@" ;;
	cat)		cloud_cat "$@" ;;
	extract|search)	cloud_extract "$@" ;;
	expand)		cloud_expand_name "$@" ;;
	*)
			print -- "$arg0: Unknown action $action." 1>&2
			usage "$goo" "$opts"
			;;
esac

exit $?

