#!/usr/bin/env sh

# Dmenu for opening a program with a specific directory.

# MIT License
#
# Copyright (c) 2020 Alexander Chaplin Braz
#
# 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.

ScriptName=${0##*/}
Version=1.0.0

# Set default bookmark if there is none:
BlankMark='---'

# Compose the help message:
HelpMessage="$ScriptName - Version: $Version
Creator: Alexander Chaplin Braz (https://alexchaplinbraz.com)
License: MIT

Usage: $ScriptName {dirpath | filepath} {programs} [mark]

This script lets the user open a directory with a program
using dmenu to select them. For this, it requires two arguments.

The first one can either be a directory from which it will find all the
subdirectories, or a file that holds a list of directories acting as bookmarks.
It's highly recommended to use absolute paths, but relative paths also work.

The second one has to be a comma separated list of programs which can be
unquoted, but quoting it gives the user the ability to specify options
for the programs.

An optional third argument lets the user change the default bookmark name
when no bookmark name was set in the bookmark file. Default: $BlankMark

The bookmark file must have a format where the path is at the start of a line.
One optional bookmark name can be set after the path by separating them with a
tab. Nothing from column 3+ is parsed. So you can use it freely to add comments
or whatever. You can also comment out lines with any character (other than \"),
thus breaking the path, so it won't show up as an option.

In the case where there's only one program and/or directory, they will be
selected automatically.

Usage examples:
    $ScriptName \$HOME vifm,lf,sxiv
    $ScriptName \ ranger,vifm
    $ScriptName \"\$HOME/Pictures\" \"sxiv -t,sxiv -arf\"
    $ScriptName \"\$HOME/Documents/dir-bookmarks.txt\" lf
    $ScriptName \"\$HOME/Documents/dir-bookmarks.txt\" lf ###

Example bookmarks file (keep in mind the tab separation):

\$HOME  HM
\"\$HOME/Pictures/Art References\"  AR
\"\$XDG_CONFIG_HOME\"   CNF
#\$COMMENT  CM  COMMENT: this can be whatever since it's not parsed.
/mnt    MNT
/etc    ETC
/backup     BU
"

suggest_help_and_exit() {
    printf "Try '%s --help' for more information.\n" "$ScriptName"
    exit 1
}

case $1 in
    '') printf '%s' "$HelpMessage"; exit 1;;
    -h|--help) printf '%s' "$HelpMessage"; exit 0;;
    -v|--version) printf '%s - Version: %s\n' "$ScriptName" "$Version"; exit 0;;
esac

# Set custom blank mark if it was set by the user:
[ "$3" ] && BlankMark=$3

# Set a location for the temporary file that holds the found directories:
TempFile="/tmp/$ScriptName-dirs"

# Set up a function that can be called to run in the background:
find_directories() {
    find "$1" -type d 2>/dev/null > "$TempFile"
    printf '%s' "$ScriptName ENDofSEARCH" >> "$TempFile"
}

# Parse the passed programs:
Parsed=$(printf '%s' "$2" | tr -s ',' '\n')

# Count the amount of programs:
ProgramAmount=$(printf '%s\n' "$Parsed" | wc -l)

# Determine whether the first argument is a directory or a file:
if [ -d "$1" ]; then
    Directory=$1
elif [ "$(file -i "$1" | awk '{printf $2}')" = 'text/plain;' ]; then
    cat "$1" | while IFS= read -r line; do
        line=$(printf '%s' "$line" \
            | awk -F '\t' -v blank=$BlankMark \
            '{print match($2, /[^ ]/) ? $2 : blank, $1}' OFS='\t')
        path=$(printf '%s' "$line" | awk -F '\t' '{printf $2}')
        eval [ -d "$path" ] 2>/dev/null \
            && printf '%s\n' "$line" >> "$TempFile"
    done
    if [ ! -s "$TempFile" ]; then
        printf 'ERROR: no valid directories in the specified file.\n'
        suggest_help_and_exit
    fi
    [ $ProgramAmount = 1 ] && pn="[$Parsed] "
    Directory=$(printf '%s' "$(cat "$TempFile")" \
        | dmenu -i -l 30 -p "${pn}Select parent directory:" \
        | awk -F '\t' '{printf $2}')
else
    printf 'ERROR: invalid first argument.\n'
    suggest_help_and_exit
fi

# Delete the temporary file:
rm "$TempFile" 2>/dev/null

# Exit if no directory was selected:
[ -z "$Directory" ] && exit 1

# Do the directory search in the background:
eval find_directories "$Directory" &

# If there's only one program, skip selecting it:
if [ $ProgramAmount = 1 ]; then
    Program=$Parsed
# Else, select the program first to hide the time taken to find the directories:
else
    Program=$(printf '%s' "$Parsed" \
        | dmenu -i -l 30 -p 'Select program:')
    [ -z "$Program" ] && exit 1
fi

# Wait until all directories have been found:
until [ "$(tail -n 1 "$TempFile")" = "$ScriptName ENDofSEARCH" ]; do
    sleep 0.1
done

# If there's only the result of the directory itself, select it:
if [ $(wc -l "$TempFile" | awk '{printf $1}') = 1 ]; then
    Chosen=$(sed '$d' "$TempFile")
# Else, choose a directory:
else
    Chosen=$(sed '$d' "$TempFile" \
        | dmenu -i -l 30 -p "[$Program] Select directory:")
fi

# Delete the temporary file:
rm "$TempFile" 2>/dev/null

# Exit if no directory was chosen:
[ -z "$Chosen" ] && exit 1

# Launch the selected program with the selected directory:
eval "$Program" \""$Chosen"\"
