#!/usr/bin/env bash

kTHIS_HOMEPAGE='https://github.com/mklement0/ttab'
kTHIS_NAME='ttab' # ${BASH_SOURCE##*/} NOTE: Hardcoded as in some cases the detected name can be a UUID, or similar (e.g. invoking ttab from Apple Shortcuts)
kTHIS_VERSION='v0.9.0' # NOTE: This assignment is automatically updated by `make version VER=<newVer>` - DO keep the 'v' prefix.

unset CDPATH  # To prevent unexpected `cd` behavior.

# --- Begin: STANDARD HELPER FUNCTIONS

die() { echo "$kTHIS_NAME: ERROR: ${1:-"ABORTING due to unexpected error."}" 1>&2; exit ${2:-1}; }
dieSyntax() { echo "$kTHIS_NAME: ARGUMENT ERROR: ${1:-"Invalid argument(s) specified."} Use --help for help." 1>&2; exit 2; }

# SYNOPSIS
#   openUrl <url>
# DESCRIPTION
#   Opens the specified URL in the system's default browser.
openUrl() {
  local url=$1
  open "$url" || { echo "Cannot locate or failed to open default browser; please go to '$url' manually." >&2; return 1; }
}

# Prints the embedded Markdown-formatted man-page source to stdout.
printManPageSource() {
  sed -n -e $'/^: <<\'EOF_MAN_PAGE\'/,/^EOF_MAN_PAGE/ { s///; t\np;}' "$BASH_SOURCE"
}

# Opens the man page, if installed; otherwise, tries to display the embedded Markdown-formatted man-page source; if all else fails: tries to display the man page online.
openManPage() {
  local pager embeddedText
  if ! man 1 "$kTHIS_NAME" 2>/dev/null; then
    # 2nd attempt: if present, display the embedded Markdown-formatted man-page source
    embeddedText=$(printManPageSource)
    if [[ -n $embeddedText ]]; then
      pager='more'
      command -v less &>/dev/null && pager='less' # see if the non-standard `less` is available, because it's preferable to the POSIX utility `more`
      printf '%s\n' "$embeddedText" | "$pager"
    else # 3rd attempt: open the the man page on the utility's website
      openUrl "${kTHIS_HOMEPAGE}/doc/${kTHIS_NAME}.md"
    fi
  fi
}

# Prints the contents of the synopsis chapter of the embedded Markdown-formatted man-page source for quick reference.
printUsage() {
  local embeddedText
  # Extract usage information from the SYNOPSIS chapter of the embedded Markdown-formatted man-page source.
  embeddedText=$(sed -n -e $'/^: <<\'EOF_MAN_PAGE\'/,/^EOF_MAN_PAGE/!d; /^## SYNOPSIS$/,/^#/{ s///; t\np; }' "$BASH_SOURCE")
  if [[ -n $embeddedText ]]; then
    # Print extracted synopsis chapter - remove backticks for uncluttered display.
    printf '%s\n\n' "$embeddedText" | tr -d '`'
  else # No SYNOPIS chapter found; fall back to displaying the man page.
    echo "WARNING: usage information not found; opening man page instead." >&2
    openManPage
  fi
}

# Indicate by exit code whether the OS version is 10.13 (High Sierra) or above.
isHighSierraOrAbove() {
  local -i major minor
  read major minor <<<$(sw_vers -productVersion | awk -F. '{ print $1; print $2 }')
  # High Sierra == macOS 10.13; Big Sur is 11.x
  (( (major == 10 &&  minor >= 13) || major >= 11 ))
}

# --- End: STANDARD HELPER FUNCTIONS

# ---  PROCESS STANDARD, OUTPUT-INFO-THEN-EXIT OPTIONS.
case $1 in
  --version)
    # Output version number and exit, if requested.
    echo "$kTHIS_NAME $kTHIS_VERSION"$'\nFor license information and more, visit '"$kTHIS_HOMEPAGE"; exit 0
    ;;
  --help)
    # Print usage information and exit.
    printUsage; exit
    ;;
  --man)
    # Display the manual page and exit, falling back to printing the embedded man-page source.
    openManPage; exit
    ;;
  --man-source) # private option, used by `make update-doc`
    # Print raw, embedded Markdown-formatted man-page source and exit
    printManPageSource; exit
    ;;
  --home)
    # Open the home page and exit.
    openUrl "$kTHIS_HOMEPAGE"; exit
    ;;
esac

# --- MAIN BODY

# Undocumented DEBUGGING SUPPORT: if the very first argument is --dry-run, we print out the synthesized AppleScript rather than executing it.
printScript=0 dryRun=0
{ [[ $1 == '--dry-run' ]] && { dryRun=1; shift; }; } || { [[ $1 == '--print-script' ]] && { printScript=1; shift; }; }

# Parse options

isMacOS=$([[ $(uname) == 'Darwin' ]] && echo 1 || echo 0)
dirAbs=''
doNotChangeDir=0
doNotTestDir=0
tabTitle=''
settingsName=''
inBackground=0
targetTermSpecified=0
inNewWin=0
inCurrent=0
inNewSplitTabH=0
inNewSplitTabV=0
cls=0
terminalApp=''
splitDirection=''
delayCmdSubmissionBy='' && kRE_FRACTIONAL_SECONDS='^[0-9]+(\.[0-9]+)?$' && kDEFAULT_CMD_DELAY='0.1'
while getopts ':wcvhgGqis:t:d:a:l:' opt; do  # $opt will receive the option *letters* one by one; a trailing : means that an arg. is required, reported in $OPTARG.
  [[ $opt == '?' ]] && dieSyntax "Unknown option: -$OPTARG"
  [[ $opt == ':' ]] && dieSyntax "Option -$OPTARG is missing its argument."
  case "$opt" in
    w)
      inNewWin=1
      ;;
    c)
      inCurrent=1
      ;;
    v)
      inNewSplitTabV=1
      splitDirection='vertically'
      ;;
    h)
      inNewSplitTabH=1
      splitDirection='horizontally'
      ;;
    i)
      doNotTestDir=1
      ;;
    s)
      settingsName=$OPTARG
      ;;
    t)
      tabTitle=$OPTARG
      ;;
    d)
      dir=$OPTARG
      [[ -z $dir ]] && doNotChangeDir=1
      if (( doNotChangeDir == 0 )); then
        if (( doNotTestDir )); then dirAbs=$dir; else { dirAbs=$(cd -- "$dir" 2>/dev/null && echo "$PWD") || die "No such directory: $dir"; } fi
      fi
      ;;
    a)
      terminalApp=$OPTARG
      targetTermSpecified=1
      ;;
    g)
      if (( inBackground )); then dieSyntax "Please specify EITHER -g OR -G"; fi
      inBackground=1
      ;;
    G)
      if (( inBackground )); then dieSyntax "Please specify EITHER -g OR -G"; fi
      inBackground=2
      ;;
    q)
      cls=1
      ;;
    l)  # "l" as in de*l*ay or *lag* - not great, but better than -y (and -d is already taken)
      delayCmdSubmissionBy=$OPTARG
      [[ $delayCmdSubmissionBy =~ $kRE_FRACTIONAL_SECONDS ]] || dieSyntax "-l argument must be 0 or a positive number in (optionally fractional) seconds."
      ;;
    *) # An unrecognized switch.
      dieSyntax "DESIGN ERROR: unanticipated option: $opt"
      ;;
  esac
done
shift $((OPTIND - 1)) # Skip the already-processed arguments (options).

# Some more options sanity check that cannot be done inplace, (e.g. their order is not guarantied)

if [[ -z $dir && doNotTestDir -eq 1 ]]; then
  # Specifying -i without also specifying -d
  dieSyntax "The -i option requires the -d option as well."
elif (( inNewSplitTabV && inNewSplitTabH )); then
  # Fail, if both `-v` and `-h` provided
  dieSyntax "Please specify EITHER -v or -h."
elif (( inNewSplitTabV || inNewSplitTabH )); then
  # Option -v and -h cannot be used, or meaningless to use with '-g', '-G', '-w' or '-c' now
  if (( inNewWin || inCurrent || inBackground )); then
    dieSyntax "The -v and -h options are mutually exclusive with the following options: -g -G -w -c"
  fi
fi

# Allow presetting the -l option via an environment variable.
if [[ -z $delayCmdSubmissionBy ]]; then
  delayCmdSubmissionBy="$TTAB_CMD_DELAY"
  [[ -z $delayCmdSubmissionBy || $delayCmdSubmissionBy =~ $kRE_FRACTIONAL_SECONDS ]] || { delayCmdSubmissionBy=''; echo "WARNING: Ignoring -l preset via env. var. TTAB_CMD_DELAY, because its value isn't a positive number of (optionally fractional) seconds." >&2; }
  # If not set by env. var., use the default delay.
  # We use a
  [[ -z $delayCmdSubmissionBy ]] && delayCmdSubmissionBy="$kDEFAULT_CMD_DELAY"
fi

# All remaining arguments, if any, make up the command to execute in the new tab/window.

# Identify the terminal application that was explicitly specified.
Terminal=0
iTerm=0
gnomeTerminal=0
if [[ -n $terminalApp ]]; then
  shopt -s nocasematch  # we want to match the application name case-INSensitively.
  case $terminalApp in
    'Terminal'|'Terminal.app')
      # Note: 'Apple_Terminal' is what $TERM_PROGRAM contains when running from Terminal.app
      # Use standard Terminal.app application.
      Terminal=1
      ;;
    'iTerm'|'iTerm.app'|'iTerm2'|'iTerm2.app')
      # Note: 'iTerm.app' is what $TERM_PROGRAM contains when running from iTerm.app
      iTerm=1
      ;;
    'gnome-terminal')
      gnomeTerminal=1
      ;;
    *)
      # Fail, if an unknown terminal was explicitly specified.
      die "'$terminalApp' is not a supported terminal application"
      ;;
  esac
  shopt -u nocasematch
fi

# Set target-terminal-app-appropriate variables used later.
while :; do
  if [[ $iTerm == 1 || $TERM_PROGRAM == 'iTerm.app' ]]; then
    iTerm=1
    terminalApp='iTerm' # will be used with `activate application`
    # Note: iTerm2's AppleScript syntax changed fundamentally in v3 (for the better, but incompatibly so),
    #       so we need to distinguish versions below.
    #       $iTermOld reflects a pre-v3 version.
    [[ $(osascript -e 'version of application "iTerm"') =~ ^(1|2) ]] && iTermOld=1 || iTermOld=0
  elif [[ $Terminal == 1 || $TERM_PROGRAM == 'Apple_Terminal' ]]; then
    Terminal=1
    terminalApp='Terminal' # will be used with `activate application`
  elif [[ $gnomeTerminal == 1 || -n $GNOME_TERMINAL_SCREEN ]]; then
    gnomeTerminal=1
    terminalApp='gnome-terminal' # binary file name
  else # The calling program is not a known terminal.
    # Determine a platform-appropriate default.
    if (( isMacOS )); then
      # Give a chance to detect the presence of iTerm2, if cannot be find fallback to Terminal.app as the default
      if [[ $(mdfind "kMDItemCFBundleIdentifier == 'com.googlecode.iterm2'" 2>/dev/null) != "" ]]; then
        iTerm=1
        terminalApp='iTerm'
      else
        Terminal=1
      fi
    else
      gnomeTerminal=1
    fi
    continue
  fi
  break
done

# Make sure that the targeted terminal app is actually present.
(( (iTerm || Terminal) && ! isMacOS )) && die "Terminal.app / iTerm2.app can only be targeted on macOS."
# Note: It's hypothetically possible to install gnome-terminal on macOS, via MacPorts.
(( gnomeTerminal )) && { which gnome-terminal &>/dev/null || die "Cannot locate Gnome Terminal's binary, gnome-terminal."; }
(( iTerm )) && {  osascript -e "version of application \"$terminalApp\"" &>/dev/null || die "Cannot locate Gnome Terminal's binary, gnome-terminal."; }
# (( Terminal )) # no need to check - Terminal.app comes with macOS.

# To be safe, clear any pre-existing variables with names matching those we'll be using below.
for n in ${!CMD_*}; do unset $n; done

  # Note for Terminal.app:
  #       We use *menu-based GUI scripting* rather than *sending keystrokes via CMD_NEWTAB_1='tell application "System Events" to keystroke "t" using {command down}'
  #       !! We try to be locale-independent, so we don't use menu-item command *names*; this requires us to make the following ASSUMPTIONS, which we expect to hold in the future:
  #             - the new *window* commands are assumed to be in a *submenu* of the *1st command* ('New Window > ...') of the Shell menu.
  #             - the new *tab* commands are assumed to be in a *submenu* of the *2nd command* ('New Tab > ...') of the Shell menu.
  #       (a) compared to sending a keystroke, this is more robust and predictable; with sending a keystroke, the shell command to execute is *sometimes* sent to the *previous* tab, as the keystroke may not have been processed yet.
  #           In the default case, the GUI scripting finds the command by its keyboard shortcut so as to work locale-independently; it does, however, make the following assumptions (other than the pretty safe assumption that the command is in the 3rd menu from the left, the "Shell" menu in English):
  #       (b) it also allows us to create new tabs *with specific settings*, by finding the menu item by *name* (settings names are locale-*in*dependent, fortunately)
  #           Using this is preferable to trying to apply settings *after* tab creation with `set current settings of selected tab ...`, as the latter only applies *visual* attributes, not *behavioral* (such as what to do when the shell exits)

if (( gnomeTerminal )); then
  if [[ -n $settingsName ]]; then
    CMD_OPT_PROFILE="--profile='${settingsName}'"
  fi
fi

if (( inNewWin )); then # create the tab in a NEW WINDOW

  if (( gnomeTerminal )); then
    CMD_NEWTAB_1='--window'
  elif (( iTerm )); then

    if (( iTermOld )); then # OLD iTerm syntax (v2-)
      # Note: Passing an empty $settingsName is accepted as specifying the default profile.
      CMD_NEWTAB_1='tell application "iTerm" to launch (make new terminal) session "'"$settingsName"'"'
      if (( inBackground == 2 )); then # For use with -G: commands for saving and restoring the previous state within iTerm
        CMD_SAVE_ACTIVE_TAB='set prevTerm to current terminal'
        CMD_REACTIVATE_PREV_TAB='set current terminal to prevTerm'
      fi
    else # NEW iTerm syntax (introduced in v3)
      if [[ -n $settingsName ]]; then
        CMD_NEWTAB_1='create window with profile "'"$settingsName"'"'
      else
        CMD_NEWTAB_1='create window with default profile'
      fi
      if (( inBackground == 2 )); then # For use with -G: commands for saving and restoring the previous state within iTerm
        CMD_SAVE_ACTIVE_TAB='set prevWin to current window'
        # !! Sadly, neither `set current window to` nor `set frontmost of <window> to true` work, so we must use GUI scripting.
        CMD_REACTIVATE_PREV_TAB='tell application "System Events" to perform action "AXRaise" of window (name of prevWin) of application process "iTerm2"'
      fi
    fi
  else # Terminal.app

    if [[ -n $settingsName ]]; then
      # !! If no such settings exist, the resulting error message can be confusing.
      CMD_NEWTAB_1='tell application "System Events" to tell menu 1 of menu item 1 of menu 1 of menu bar item 3 of menu bar 1 of application process "Terminal" to click menu item "'"$settingsName"'"'
      CMD_NEWTAB_2='set newTab to selected tab of front window'
    else
      CMD_NEWTAB_1=''
      CMD_NEWTAB_2='set newTab to do script' # note that running `do script` without arguments (i.e., without a suffix specifying the target window (e.g., 'in front window')) creates a *new* window.
    fi
    if (( inBackground == 2 )); then # For use with -G: commands for saving and restoring the previous state within Terminal
      CMD_SAVE_ACTIVE_TAB='set prevWin to front window'
      CMD_REACTIVATE_PREV_TAB='set frontmost of prevWin to true' # Note that while  *Terminal.app*'s  windows have a `frontmost` property that allows their activation within the app, most other (all other?) apps' windows do not.
    fi

  fi

elif (( inNewSplitTabV || inNewSplitTabH )); then # Create the new split tab in the CURRENT WINDOW

  # iTerm only: Split tabs are supported only in iTerm currently
  # TODO: We might want to support solutions like tmux as well
  if (( iTerm )); then # only NEW iTerm syntax has split option (I guess)
    if [[ -n $settingsName ]]; then
      CMD_NEWTAB_1='tell current session of current window to set newSplit to split '$splitDirection' with profile "'"$settingsName"'"'
    else
      CMD_NEWTAB_1='tell current session of current window to set newSplit to split '$splitDirection' with default profile'
    fi
    CMD_NEWTAB_2='select newSplit'
  else
    dieSyntax "Vertical or horizontal tab splitting is not supported in $terminalApp."
  fi

elif (( inCurrent == 0 )); then  # If should not use the actual, active tab, by default create a new tab in the CURRENT WINDOW (unlike `do shell script` with no target for creating a new *window*, `do shell script in front window` simply executes in the *current* tab, and there's no direct way to target a new one).

  if (( gnomeTerminal )); then

    CMD_NEWTAB_1='--tab'

  elif (( iTerm )); then

    if (( iTermOld )); then # OLD iTerm syntax (v2-)
      # Note: Passing an empty $settingsName is accepted as specifying the default profile.
      CMD_NEWTAB_1='tell application "iTerm" to launch (current terminal) session "'"$settingsName"'"'
      if (( inBackground == 2 )); then # For use with -G: commands for saving and restoring the previous state within iTerm
        CMD_SAVE_ACTIVE_TAB='set prevSess to current session of current terminal'
        CMD_REACTIVATE_PREV_TAB='select prevSess'
      fi
    else # NEW iTerm syntax (introduced in v3)
      if [[ -n $settingsName ]]; then
        CMD_NEWTAB_1='tell current window to create tab with profile "'"$settingsName"'"'
      else
        CMD_NEWTAB_1='tell current window to create tab with default profile'
      fi
      if (( inBackground == 2 )); then # For use with -G: commands for saving and restoring the previous state within iTerm
        CMD_SAVE_ACTIVE_TAB='set prevTab to current tab of current window'
        CMD_REACTIVATE_PREV_TAB='select prevTab'
      fi
    fi

  else # Terminal.app

    if [[ -n $settingsName ]]; then
        CMD_NEWTAB_1='tell application "System Events" to tell menu 1 of menu item 2 of menu 1 of menu bar item 3 of menu bar 1 of application process "Terminal" to click menu item "'"$settingsName"'"'
    else
        CMD_NEWTAB_1='tell application "System Events" to tell menu 1 of menu item 2 of menu 1 of menu bar item 3 of menu bar 1 of application process "Terminal" to click (first menu item whose value of attribute "AXMenuItemCmdChar" is "T" and value of attribute "AXMenuItemCmdModifiers" is 0)'
    fi
    CMD_NEWTAB_2='set newTab to selected tab of front window'
    if (( inBackground == 2 )); then # For use with -G: commands for saving and restoring the previous state within Terminal
      if isHighSierraOrAbove; then
        # !! In 10.13 (High Sierra), Terminal.app switched to treating tabs like individual *windows*.
        CMD_SAVE_ACTIVE_TAB='set prevTab to front window'
        CMD_REACTIVATE_PREV_TAB='set frontmost of prevTab to true'
      else
        CMD_SAVE_ACTIVE_TAB='set prevTab to (selected tab of front window)'
        CMD_REACTIVATE_PREV_TAB='set selected of prevTab to true'
      fi
    fi

  fi

else # In current tab (-c)

  if (( Terminal )); then
    CMD_NEWTAB_1='set newTab to selected tab of front window'
  elif (( gnomeTerminal && inCurrent )); then
    dieSyntax "The '-c' option is not supported in $terminalApp."
  fi

fi

  # Terminal/iTerm only:
  # Define the command that *synchronously* actives iTerm / Terminal.
  # Note that this is neeeded both with and without -g / -G:
  #   * With -g / -G, unfortunately, the terminal app must still be activated briefly
  #     in order for GUI scripting to work correctly, with the previously active
  #     application getting reactivated afterward.
  #   * With foregound operations, we also activate explicitly, so as to support
  #     invocation from helper apps such as Alfred where the terminal may be
  #     created implicitly and not gain focus by default.
  # !! On 10.10+, activate is no longer *synchronous*, so we must wait until Terminal is truly activated (frontmost)
  # !! iTerm has a setting that hides the application from the dock and from being switched to with ⌘-tab
  # !! (Appearance > General > Exclude from Dock and ⌘-Tab Application Switcher). When active, AppleScript can no longer
  # !! detect when iTerm is frontmost, which prevents:
  # !!  * synchronously waiting for iTerm to become active
  # !!  * the -g / -G options from working (they rely on saving the currently frontmost application, and if it is iTerm, that won't work)
  # !! Thus, we simply forgo synchronous activation in this case, and disallow use of the -g / -G options.
canDetectActivation=1
if (( iTerm )); then
  if [[ '1' == $(defaults read com.googlecode.iterm2 HideFromDockAndAppSwitcher 2>/dev/null) ]]; then canDetectActivation=0; fi
  if (( ! canDetectActivation && inBackground )); then
    die "While iTerm's 'Exclude from Dock and ⌘-Tab Application Switcher' feature is turned on, the options -g and -G are unsupported."
  fi
fi
CMD_ACTIVATE="if not frontmost then
  activate
  if $canDetectActivation = 1 then
    repeat until frontmost
      delay 0.1
    end repeat
  end if
end if"

  # Terminal/iTerm only:
  # For use with -g and -G: commands for saving and restoring the previous application
if (( inBackground )); then
  CMD_SAVE_ACTIVE_APPNAME='set prevAppPath to path to frontmost application as text'
  CMD_REACTIVATE_PREV_APP='activate application prevAppPath'
fi

# Note: The desired behavior is to ALWAYS OPEN A TAB IN THE DIRECTORY THE CALLER
#       CONSIDERS CURRENT, whether the new tab is being opened in the current or
#       a new window (unless a target dir. is explicitly specified with -d <dir>).
#       Terminal and iTerm have different default behaviors, so we need to account for
#       that:
#         * When opening a tab in a new *window*, both Terminal and iTerm default to the *home* dir.
#         * When opening a new tab in the *current window*,
#             * iTerm: defaults to the home dir.
#             * Terminal: the *caller's currrent dir., as known to Terminal* (see below) is used.
#       Also, to be safe, if a target terminal is explicitly specified, we also
#       default to issuing setting the current dir. explicitly, because it might be a different terminal than the current one.
if (( iTerm || targetTermSpecified )); then
  # iTerm2 always defaults to the home dir., so we must always add an explicit `cd` command to ensure that the current dir. is used.
  if [[ -z $dirAbs ]]; then
    dirAbs=$PWD
  fi
else
  if (( gnomeTerminal )); then
    : # Nothing to do, because gnome-terminal always defaults to the caller's current dir.
  else # Terminal.app
    # While Terminal.app does default to the caller's current dir. when creating a tab
    # in the *current* window, it doesn't necessarily know the *immediate caller's* true $PWD,
    # so we have to compensate for that:
    #   Terminal.app only knows the working directory of the *top-level* shell running in each tab (as it defines an aux. function,
    #   update_terminal_cwd(), that is called via $PROMPT_COMMAND every time the prompt is displayed).
    #   Thus, when this script is invoked inside another script, it is the *top-level* shell's working directory that is invariably used by
    #   Terminal, even if the invoking script has changed the working directory. Since this is counter-intuitive, we compensate here
    #   by explicitly setting the working directory to the invoking script's with a prepended 'cd' command.
    #   $SHLVL tells us the nesting level of the current shell:
    #   1 == top-level shell; since this script itself runs in a subshell (2, if invoked directly from the top-level shell), we can safely assume
    #   that another *script* has invoked us, if $SHLVL >= 3.
    if [[ -z $dirAbs && ($SHLVL -ge 3 || $inNewWin -eq 1) ]]; then
      dirAbs=$PWD
    fi
  fi
fi

  # Optional commands that are only used if the relevant options were specified.
quotedShellCmds=''
shellCmdTokens=( "$@" )
if (( ${#shellCmdTokens[@]} )); then # Shell command(s) specified.

  if (( ${#shellCmdTokens[@]} == 1 )); then # Could be a mere command name like 'ls' or a multi-command string such as 'git bash && git status'
    # If only a single string was specified as the command to execute in the new tab:
    # It could either be a *mere command name* OR a *quoted string containing MULTIPLE commands*.
    # We use `type` to determine if it is a mere command name / executable in the
    # current dir., otherwise we assume that the operand is a *multi-command string*
    # in which case *we must use `eval` to execute it*.
    # Note: *Blindly* prepending `eval` is not an option, because it would only work in MOST, but NOT ALL cases,
    #       such as not with commands whose names happen to contain substrings
    #       that look like variable references (however rare that may be).
    ([[ -n $dirAbs ]] && cd -- "$dirAbs" # Change to the target dir. first, to accommodate calls such as ./foo that assume to be running in the target dir.
    # Note: Since we're forced to use $SHELL -c in gnome-terminal anyway, we do NOT need `eval` there.
    type "${shellCmdTokens[0]}" &>/dev/null) || { (( ! gnomeTerminal )) && shellCmdTokens=( 'eval' "${shellCmdTokens[@]}" ); }
  fi

  # Quote (escape) the custom command.
  # !! The use of printf '%q' with its \-based escaping means that
  # !! shells with different escaping rules are NOT supported, notably PowerShell.
  if (( gnomeTerminal )); then
    # Form a single, quoted string that will be passed to $SHELL -c
    quotedShellCmds=$(printf '%q' "${shellCmdTokens[*]}")
  else
        # The tricky part is to quote the command tokens properly when passing them to AppleScript:
        # Quote all parameters (as needed) using printf '%q' - this will perform backslash-escaping.
        # This will allow us to not have to deal with double quotes inside the double-quoted string that will be passed to `do script`.
    quotedShellCmds=$(printf ' %q' "${shellCmdTokens[@]}")
    # Note: $quotedShellCmds now has a leading space, but that's benign (a *trailing* space, by contrast, would be a problem with iTerm's `write <session> text ...` command)
  fi

  # !! [AUTO-DERIVING A TAB TITLE DISABLED - there's ultimately no heuristic that's guaranteed to result in a meaningful title. Let users specify a title explicitly, if needed. ]

fi # if (( ${#shellCmdTokens[@]} )


# Add a commmand / option to change the working dir., if needed.
if (( gnomeTerminal )); then # gnome-terminal has a dedicated option

  # gnome-terminal *always* uses the caller's working dir.
  [[ -n $dirAbs ]] && CMD_OPT_CWD="--working-directory=\"$dirAbs\""

else # Terminal/iTerm

  # Prepend the 'cd' command, if specified or needed - unless suppressed.
  if [[ $doNotChangeDir -eq 0 && -n $dirAbs ]]; then
    quotedCdCmd=$(printf 'cd %q' "$dirAbs")
    if [[ -n $quotedShellCmds ]]; then
      quotedShellCmds="$quotedCdCmd;$quotedShellCmds"
    else
      quotedShellCmds=$quotedCdCmd
    fi
  fi

fi

# Append the 'clear' command, if requested.
if (( cls )); then
  if [[ -n $quotedShellCmds ]]; then
    if (( gnomeTerminal )); then
      # Note: Since we're forced to use $SHELL -c in gnome-terminal, $quotedShellCmds
      #       an *unquoted* string with \-escaping
      quotedShellCmds="$quotedShellCmds\ \&\&\ clear"
    else
      quotedShellCmds="$quotedShellCmds && clear"
    fi
  else
    quotedShellCmds='clear'
  fi
fi


# Synthesize the full shell command.
if [[ -n $quotedShellCmds ]]; then

  # Terminal/iTerm only:
  # Pass the commands as a single AppleScript string, of necessity double-quoted.
  # For the benefit of AppleScript
  #  - embedded backslashes must be escaped by doubling them
  #  - embedded double quotes must be backlash-escaped
  quotedShellCmdsForAppleScript=${quotedShellCmds//\\/\\\\}
  quotedShellCmdsForAppleScript=${quotedShellCmdsForAppleScript//\"/\\\"}

  if (( gnomeTerminal )); then
    CMD_CUSTOM="$quotedShellCmds"
  elif (( iTerm )); then
    if (( iTermOld )); then # OLD iTerm syntax (v2-)
      CMD_CUSTOM="write (current session of current terminal) text \"${quotedShellCmdsForAppleScript}\""
    else # NEW iTerm syntax (introduced in v3)
      CMD_CUSTOM="tell current session of current window to write text \"${quotedShellCmdsForAppleScript}\""
    fi
  else # Terminal.app
    CMD_CUSTOM="do script \"${quotedShellCmdsForAppleScript}\" in newTab"
  fi

fi

if [[ -n $tabTitle ]]; then # custom tab title specified
  # For the benefit of AppleScript
  #  - embedded backslashes must be escaped by doubling them
  #  - embedded double quotes must be backlash-escaped
  tabTitle=${tabTitle//\\/\\\\}
  tabTitle=${tabTitle//\"/\\\"}
  if (( gnomeTerminal )); then
    CMD_TITLE="--title=\"$tabTitle\""
  elif (( iTerm )); then
    if (( iTermOld )); then # OLD iTerm syntax (v2-)
      CMD_TITLE="tell current session of current terminal to set name to \"$tabTitle\""
    else # NEW iTerm syntax (introduced in v3)
      CMD_TITLE="tell current session of current window to set name to \"$tabTitle\""
    fi
  else  # Terminal.app
    CMD_TITLE="set custom title of newTab to \"$tabTitle\""
  fi
fi

if (( gnomeTerminal )); then

  (( inBackground == 1 )) && dieSyntax "The -g option is not supported in $terminalApp."

  # The new tab should become active, unless -G is specified.
  (( inBackground == 2 )) || CMD_OPT_ACTIVATE='--active'

  # Append a custom command, if specified.
  # !! gnome-terminal does NOT support submitting a command to be run in *whatever shell is configured for the target profile*.
  # !! Instead, a shell executable must be explicitly specified, and - given that POSIX-compatible shells do not support executing
  # !! a command with -c *and staying open* (not exiting) afterwards, the best we can do is to append `; exec $SHELL` to the -c
  # !! call, which is only an *approximation* of staying in the same shell, however.
  if [[ -n $CMD_CUSTOM ]]; then
    if [[ (${#shellCmdTokens[@]} -gt 1 && ${shellCmdTokens[0]} == 'exec') || (${#shellCmdTokens[@]} -eq 1 && ${shellCmdTokens[0]} =~ ('&&'|'||'|';')\ *exit\ *\;?\ *$) ]]; then
      # !! If the command starts with `exec` or ends in `exit`, we infer that the intent
      # !! is NOT to keep the tab open; since $SHELL -c automatically exits by default, there's nothing else to do.
      CMD_OPT_CUSTOM="-- $SHELL -c $CMD_CUSTOM"
    else
      # !! Artifically keep the tab open by re-executing $SHELL in interactive mode.
      # !! The shell environment of the original shell cannot be fully preserved that way.
      # !! Note the use of `trap` with signals EXIT (normal exit) and INT (Ctrl-C) to ensure
      # !! that the new interactive shell is also launched when the original shell is terminated with Ctrl-C.
      # !! Verified to work in dash, bash, ksh, and zsh (in bash, EXIT alone would cover Ctrl-C, but not in zsh, for instance).
      CMD_OPT_CUSTOM="-- $SHELL -c trap\ \'exec\ $SHELL\'\ INT\ EXIT\;\ $CMD_CUSTOM"
    fi
  fi

  script="$terminalApp $CMD_NEWTAB_1 $CMD_OPT_ACTIVATE $CMD_OPT_CWD $CMD_OPT_PROFILE $CMD_TITLE $CMD_OPT_CUSTOM"

else # Terminal/iTerm2

  # Insert a delay before submitting a custom command and/or directory-changing command via AppleScript
  # Note: Fortunately, AppleScript's `delay` command supports *fractional* seconds.
  CMD_DELAY=
  [[ -n $CMD_CUSTOM && -n $delayCmdSubmissionBy ]] && CMD_DELAY="delay $delayCmdSubmissionBy"

  # Now, synthesize the entire AppleScript
  # !! At least on 10.10, the commands to save the active application and to reactivate it later must be OUTSIDE of the tell application "Terminal" block to work.
  read -d '' -r script <<EOF
  $CMD_SAVE_ACTIVE_APPNAME
  tell application "$terminalApp"
    $CMD_ACTIVATE
    $CMD_SAVE_ACTIVE_TAB
    $CMD_NEWTAB_1
    $CMD_NEWTAB_2
    $CMD_TITLE
    $CMD_DELAY
    $CMD_CUSTOM
    $CMD_REACTIVATE_PREV_TAB
  end tell
  $CMD_REACTIVATE_PREV_APP
  return
EOF

fi

(( dryRun || printScript )) && { printf '%s\n' "$script"; (( dryRun )) && exit; }

# Execute the synthesized command line / AppleScript.
if (( gnomeTerminal )); then
  eval $script
  ec=$?
else
  # Note: By using `exec` we pass `osascript`'s exit code through and
  #       END EXECUTION HERE, so that that the `: <<...` here-doc for the man page
  #       below doesn't reset the exit code to 0.
  exec osascript <<<"$script"
fi

####
# MAN PAGE MARKDOWN SOURCE
#  - Place a Markdown-formatted version of the man page for this script
#    inside the here-document below.
#    The document must be formatted to look good in all 3 viewing scenarios:
#     - as a man page, after conversion to ROFF with marked-man
#     - as plain text (raw Markdown source)
#     - as HTML (rendered Markdown)
#  Markdown formatting tips:
#   - GENERAL
#     To support plain-text rendering in the terminal, limit all lines to 80 chars.,
#     and, for similar rendering as HTML, *end every line with 2 trailing spaces*.
#   - HEADINGS
#     - For better plain-text rendering, leave an empty line after a heading.
#       marked-man will remove it from the ROFF version.
#     - The first heading must be a level-1 heading containing the utility
#       name and very brief description; append the manual-section number
#       directly to the CLI name; e.g.:
#         # foo(1) - does bar
#     - The 2nd, level-2 heading must be '## SYNOPSIS' and the chapter's body
#       must render reasonably as plain text, because it is printed to stdout
#       when  `--help` is specified:
#         Use 4-space indentation without markup for both the syntax line and the
#         block of brief option descriptions; represent option-arguments and operands
#         in angle brackets; e.g., '<foo>'
#     - All other headings should be level-2 headings in ALL-CAPS.
#   - TEXT
#      - Use NO indentation for regular chapter text; if you do, it will
#        be indented further than list items.
#      - Use 4-space indentation, as usual, for code blocks.
#      - Markup character-styling markup translates to ROFF rendering as follows:
#         `...` and **...** render as bolded (red) text
#         _..._ and *...* render as word-individually underlined text
#   - LISTS
#      - Indent list items by 2 spaces for better plain-text viewing, but note
#        that the ROFF generated by marked-man still renders them unindented.
#      - End every list item (bullet point) itself with 2 trailing spaces too so
#        that it renders on its own line.
#      - Avoid associating more than 1 paragraph with a list item, if possible,
#        because it requires the following trick, which hampers plain-text readability:
#        Use '&nbsp;<space><space>' in lieu of an empty line.
####
: <<'EOF_MAN_PAGE'
# ttab(1) - open a new terminal tab or window

## SYNOPSIS

Opens a new terminal tab or window, on macOS in either Terminal.app or  
iTerm2.app; on Linux in Gnome Terminal, if available.

    ttab [-w|-v|-h|-c] [-i] [-s <settings>] [-t <title>] [-q] [-g|-G] [-d <dir>] [<cmd> ...]

    -w                  Open new tab in new terminal window.
    -v                  iTerm only: create a new vertical split
    -h                  iTerm only: create a new horizontal split
    -c                  Terminal/iTerm only: do not open any new window or
                        tab, run in the current tab of the current window.
    -i                  Suppress up-front verification of the existence of
                        the target directory specified via the -d option.
    -s <settings>       Assign a settings set (profile).
    -t <title>          Specify title for new tab.
    -q                  Clear the new tab's screen.
    -g                  Terminal/iTerm only: create in background (don't
                        activate application).
    -G                  Terminal/iTerm: don't activate new tab and create in
                        background.
                        gnome-terminal: don't activate new tab, except with -w.
    -d <dir>            Specify working directory; -d '' disables inheriting
                        the current dir. in Terminal/iTerm.
    -l <secs>           Terminal/iTerm only: delay startup command submission;
                        may be preset via env. var. TTAB_CMD_DELAY
    -a Terminal | iTerm Open the new tab in the given terminal app on macOS.
    <cmd> ...           Command to execute in the new tab.
    "<cmd> ...; ..."    Multi-command command line (passed as single operand).

Standard options: `--help`, `--man`, `--version`, `--home`

## DESCRIPTION

`ttab` opens a new terminal tab with a variety of options,  
including executing a command in the new tab, assigning a title and working  
directory, and opening the tab in a new window.

Supports Terminal.app and iTerm2.app on macOS, and - with limitations -  
gnome-terminal on Linux.

Note: iTerm2 and gnome-terminal support is currently not covered by the  
automated tests run before every release.

IMPORTANT: On macOS, **Terminal/iTerm2 must be allowed assistive access** in  
order for this  utility to work, which requires one-time authorization with  
administrative privileges. If you get error messages instead of being prompted,  
authorize the application via  
System Preferences > Security & Privacy > Privacy > Accessibility.

The new tab or window inherits the calling shell's working directory by default.  
On macOS, the new tab or window runs a login shell (i.e., loads the user's  
shell profile) and inherits the calling shell's working directory.

When specifying a command to execute in the new tab, quoted parameters are  
handled properly and there's no need to quote the command as a whole,  
provided it is a *single* command.  
Prefix such a single command with `exec` to exit the shell after the command  
terminates. If the tab's settings are configured to close tabs on termination  
of the shell, the tab will close automatically.

To specify *multiple* commands, pass them as a single command-line string.  
Use `exit` as the last command to automatically close the tab when the  
command terminates, assuming the tab's settings are configured to close the  
tab on termination of the shell.  
Precede `exit` with `read -rsn 1` to wait for a keystroke first.

IMPORTANT: Specifying a command to execute in the new tab has limitations:  

* Specifying a startup command is only supported for POSIX-compatible shells,  
  because "\" is used for behind-the-scenes escaping, and the presence of the  
  `eval` / `exec` builtins is assumed on macOS / Linux.  
  Notably, this precludes passing commands to PowerShell.

* With gnome-terminal (Linux), $SHELL, the user's default shell is invariably  
  used to execute the command and to run in the new tab, even if the target  
  profile (settings) is configured to run a different shell.  
  Also, a workaround is employed to keep the tab open: because the $SHELL must  
  be invoked with -c in order to process the command, it exits on completion,  
  so `exec $SHELL` is  executed afterwards to keep the tab open (unless you  
  pass a single `exec` command or multi-command string that ends with `exit`);  
  this second $SHELL instance cannot guarantee that the original's shell  
  environment is fully preserved.

## OPTIONS

 * `-w`  
    creates the new tab in a new window rather than in the front  
    window.

 * `-c`  
    Terminal/iTerm2 only:  
    does not create any new window or tab, and uses the current tab in the  
    front window.  
    This is primarily useful when launching this utility from a macOS service  
    or Shortcuts.app shortcut, for targeting the target terminal's current tab.  

 * `-h`  
    iTerm2 only:  
    creates a new horizontal split in the current tab.  

 * `-v`  
    iTerm2 only:  
    creates a (new) vertical split in the current tab.  

 * `-i`  
    suppresses up-front verification of the existence of the target directory  
    specified via the -d option.  
    This is primarily useful on macOS if this utility is launched from a  
    non-privileged session (e.g., a Shortcuts.app shortcut) that may not have  
    access to the target directory, whereas the targeted terminal may.  

 * `-s <settings>`  
    specifies the settings set (profile) to apply to the new tab, determining  
    the appearance and behavior of the new tab.  
    o Terminal: settings sets are defined in Preferences > Profiles;  
    name matching is case-*in*sensitive, and specifying nonexistent settings  
    causes an error.  
    o iTerm2: profiles are defined in Preferences > Profiles; name matching  
    is case-*sensitive*, and specifying a nonexistent profile causes an error.
    o gnome-terminal: profiles are defined in Edit > Preferences; name matching  
    is case-*sensitive*, and specifying a nonexistent profile falls back to  
    to the default profile.

 * `-t <title>`   
    specifies a custom title to assign to the new tab.    

 * `-d <dir>`  
    explicitly specifies a working directory for the new tab; by default, the  
    invoking shell's working directory is inherited; in Terminal/iTerm, you  
    can use `-d ''` to disable this inheriting.

 * `-q`  
    (*q*uiet) issues a `clear` command after opening the new tab.  
    Note that output will temporarily be visible while the tab is being opened;  
    also, clearing is not performed if any command passed reports an overall  
    nonzero exit code, so as to allow failures to be examined.

 * `-g`  
    Terminal/iTerm2 only:  
    (back*g*round) causes Terminal/iTerm2 not to activate, if it isn't the  
    frontmost application); within the application, however, the new tab will  
    become the active tab; useful in scripts that launch other applications and  
    don't want Terminal/iTerm2 to steal focus later.

 * `-G`  
    Terminal/iTerm2:
      causes Terminal/iTerm2 not to activate *and* the active element within  
      the application not to change; i.e., the active window and tab stay the  
      same. If Terminal/iTerm2 happens to be frontmost, the new tab will  
      effectively open in the background.
    gnome-terminal:  
      causes the new tab not to activate except if it is created in a  
      new window (-w).

NOTE: Terminal/iTerm2: With `-g` or `-G`, the new tab will still activate  
      *briefly, temporarily*, for technical reasons. The temporary activation  
      lasts as least as long as the effective -l value (command-submission  
      delay).

* `-l <secs>`  
    Terminal/iTerm2 only:  
    delays submitting a startup command by the specified number  
    of seconds (fractions supported); useful for shells with initialization  
    files that take a long time to process. 
    Note that setting a working dir. with -d alone entails execution of a  
    startup command (`cd`), and that in iTerm2 such a startup command is run  
    even by default, to ensure that the caller's working dir. is inherited.  
    The default is 0.1 secs; you can preset a different value via  
    environment variable TTAB_CMD_DELAY. Note the impact on -g / -G.

* `-a Terminal` or `-a iTerm2`  
    explicitly specifies which terminal application to use on macOS;  
    by default, the terminal application from which this utility is run is  
    implied, if supported, with Terminal / gnome-terminal used as the default  
    on  macOS / Linux.  
    This option is useful for calling this utility from non-terminal  
    applications such as Alfred (https://www.alfredapp.com/) on macOS.

## STANDARD OPTIONS

All standard options provide information only.

 * `--help`  
    Prints the contents of the synopsis chapter to stdout for quick reference.

 * `--man`  
    Displays this manual page, which is a helpful alternative to using `man`, 
    if the manual page isn't installed.

 * `--version`  
    Prints version information.
    
 * `--home`  
    Opens this utility's home page in the system's default web browser.

## LICENSE

For license information and more, visit this utility's home page by running  
`ttab --home`.

## EXAMPLES

    # Open new tab in current terminal window:
    ttab

    # Open new tab in new terminal window:
    ttab -w

    # Open new horizontal split tab in the current terminal window:
    ttab -h

    # Open new vertical split tab in the current terminal window, changing
    # the path to the given path, but without checking the presence of the 
    # target directory:
    ttab -v -i -d "$HOME/Library/Application Support"

    # Open new tab with title 'Green' using settings (profile) 'Grass':
    ttab -t Green -s Grass  

    # Open new tab and execute a command in it:
    ttab ls -l "$HOME/Library/Application Support"

    # Open new tab with specified working dir. and execute a command in it:
    ttab -d "$HOME/Library/Application Support" ls -l
    
    # Execute a command and exit.
    # If configured via the default profile, also close the tab.
    ttab exec /path/to/someprogram arg1 arg2
    
    # Pass a multi-command string as a single, quoted string, wait for a  
    # keystroke, then exit.
    ttab 'ls "$HOME/Library/Application Support";
                                echo Press any key to exit; read -rsn 1; exit'

    # Create a new tab explicitly in iTerm2.
    ttab -a iTerm2 echo "Hi from iTerm2."
    
    # Execute the command in the current tab of the current terminal window:
    ttab -c 'ls "$HOME/Library/Application Support"'
EOF_MAN_PAGE

exit $ec
