#!/usr/bin/env sh
:; # -*- mode: emacs-lisp; lexical-binding: t -*-
:; case "$EMACS" in *term*) EMACS=emacs ;; *) EMACS="${EMACS:-emacs}" ;; esac
:; [ "x$EMACS" = xemacs ] && { type emacs >/dev/null 2>&1 || err=1; }
:; [ "x$err" = x ] || { echo "Error: failed to run Emacs with command '$EMACS'"; echo; echo "Are you sure Emacs is installed and in your \$PATH?"; exit 1; } >&2
:; emacs="$EMACS ${DEBUG:+--debug-init} -q --no-site-file --batch"
:; export EMACS_PLUS_NO_PATH_INJECTION=1
:; export __DOOMPID="${__DOOMPID:-$$}"
:; export __DOOMSTEP="${__DOOMSTEP:-0}"
:; export __DOOMGEOM="${__DOOMGEOM:-`tput cols 2>/dev/null`x`tput lines 2>/dev/null`}"
:; export __DOOMGPIPE=${__DOOMGPIPE:-$__DOOMPIPE}
:; export __DOOMPIPE=; [ -t 0 ] || __DOOMPIPE="${__DOOMPIPE}0"; [ -t 1 ] || __DOOMPIPE="${__DOOMPIPE}1"
:; eval "$emacs --eval \"(setq warning-inhibit-types '((files missing-lexbind-cookie)))\" --load" "$0" -- "$@" || exit=$?
:; [ "${exit:-0}" -eq 254 ] && { export TMPDIR="${TMPDIR:-${TMP:-${TEMP:-`eval "$emacs -Q --eval '(princ temporary-file-directory)'" 2>/dev/null`}}}"; sh "${TMPDIR}/doom.${__DOOMPID}.${__DOOMSTEP}.sh" "$0" "$@" && true; exit="$?"; }
:; exit $exit

;; This magical mess of a shebang is necessary for any script that relies on
;; Doom's CLI framework, because Emacs' tty libraries and capabilities are too
;; immature (borderline non-existent) at the time of writing (28.1). This
;; shebang sets out to accomplish these three goals:
;;
;; 1. To produce a more helpful error if Emacs isn't installed or broken. It
;;    must do so without assuming whether $EMACS is a shell command (e.g. 'snap
;;    run emacs') or an absolute path (to an emacs executable). I've avoided
;;    'command -v $EMACS' for this reason.
;;
;; 2. To allow this Emacs session to "exit into" a child process (since Elisp
;;    lacks an analogue for exec system calls) by calling an auto-generated and
;;    self-destructing "exit script" if the parent Emacs process exits with code
;;    254. It takes care to prevent nested child instances from clobbering the
;;    exit script.
;;
;; 3. To expose some information about the terminal and session:
;;    - $__DOOMGEOM holds the dimensions of the terminal (W . H).
;;    - $__DOOMPIPE indicates whether the script has been piped (in and/or out).
;;    - $__DOOMGPIPE indicates whether one of this process' parent has been
;;      piped to/from.
;;    - $__DOOMPID is a unique identifier for the parent script, so
;;      child processes can identify which persistent data files (like logs) it
;;      has access to.
;;    - $__DOOMSTEP counts how many levels deep we are in the dream (appending
;;      this to the exit script's filename avoids child processes clobbering the
;;      same exit script and causing read errors).
;;    - $TMPDIR (or $TEMP and $TMP on Windows) aren't guaranteed to have values,
;;      and mktemp isn't available on all systems, but you know what is? Emacs!
;;      So I use it to print `temporary-file-directory'. And it seconds as a
;;      quick sanity check for Emacs' existence (for goal #1).
;;
;; Other weird facts about this shebang line:
;;
;; - The :; hack exploits properties of : and ; in shell scripting and elisp to
;;   allow shell script and elisp to coexist in the same file without either's
;;   interpreter throwing foreign syntax errors:
;;
;;   - In elisp, ":" is a valid keyword symbol literal; it evaluates to itself
;;     and has no side-effect.
;;   - In the shell, ":" is a valid command that does nothing and ignores its
;;     arguments.
;;   - In elisp, ";" begins a comment. I.e. the interpreter ignores everything
;;     after it.
;;   - In the shell, ";" is a command separator.
;;
;;   Put together, plus a strategically placed exit call, the shell will read
;;   one part of this file and ignore the rest, while the elisp interpreter will
;;   do the opposite.
;; - I intentionally suppress loading site files (must be done with '-q
;;   --no-site-file' NOT '-Q', as the latter omits the site-lisp dir from
;;   `load-path' too), so lisp/doom-cli.el can load them manually later (early
;;   in lisp/doom-cli.el). Look there for an explanation why I do this.
;; - POSIX-compliancy is paramount: there's no guarantee what /bin/sh will be
;;   symlinked to in the esoteric OSes/distros Emacs users use.
;; - The user may have mounted /tmp with a noexec flag, so pass the exit script
;;   to /bin/sh rather than executing them directly.

;; Doom's core sets up everything we need; including `doom-*-dir' variables,
;; universal defaults, and autoloads for doom-*-initialize functions.
(condition-case e
    (let* ((bin-dir (file-name-directory (file-truename load-file-name)))
           (init-file (expand-file-name "../early-init.el" bin-dir)))
      (or (and (load init-file nil 'nomessage 'nosuffix)
               (featurep 'doom))
          (user-error "Failed to load Doom from %s" init-file)))
  ;; Prevent ugly backtraces for trivial errors
  (user-error (message "Error: %s" (cadr e))
              (kill-emacs 2)))

;; UX: Abort if the user is using 'doom' as root, unless $EMACSDIR is owned by
;;   root, in which case we can safely assume the user genuinely wants root to
;;   be their primary user account for this session.
(when (equal 0 (user-real-uid))
  (unless (equal 0 (file-attribute-user-id (file-attributes doom-emacs-dir)))
    (message
     (concat
      "Error: this script was executed as root, which is likely not what you want.\n"
      "It will cause file permissions errors later, when you run Doom as another\n"
      "user.\n\n"
      "If this really *is* what you want, then change the owner of your Emacs\n"
      "config to root:\n\n"
      ;; TODO Add cmd.exe/powershell commands
      "  chown root:root -R " (abbreviate-file-name doom-emacs-dir) "\n\n"
      "Aborting..."))
    (kill-emacs 2)))


;;
;;; Entry point

(defcli! doom (&args _command)
  "A command line interface to Doom Emacs.

Includes package management, diagnostics, unit tests, and byte-compilation.

This tool also makes it trivial to launch Emacs out of a different folder or
with a different private module.

ENVIRONMENT VARIABLES:
  `$EMACS'
    The Emacs executable or command to use for any Emacs operations in this or
    other Doom CLI shell scripts (default: first emacs found in `$PATH').

  `$EMACSDIR'
    The location of your Doom Emacs installation (defaults to ~/.config/emacs or
    ~/.emacs.d; whichever is found first). This is *not* your private Doom
    configuration. The `--emacsdir' option also sets this variable.

  `$DOOMDIR'
    The location of your private configuration for Doom Emacs (defaults to
    ~/.config/doom or ~/.doom.d; whichever it finds first). This is *not* the
    place you've cloned doomemacs/doomemacs to. The `--doomdir' option also sets
    this variable.

  `$DOOMPAGER'
    The pager to invoke for large output (default: \"less +g\"). The `--pager'
    option also sets this variable.

  `$DOOMPROFILE'
    Which Doom profile to activate (default: \"_@0\"). The `--profile' option
    also sets this variable.

  `$DOOMPROFILELOADFILE'
    Doom generates a profile loader script on 'doom sync' or 'doom profile
    sync'. By default, this file is written to and loaded from
    $EMACSDIR/profiles/load.el. Set this envvar to change that. Note that this
    envvar must be in scope for both interactive and non-interactive sessions
    for it to be effective. This is especially useful for folks on Nix/Guix, who
    have deployed Doom to a read-only directory.

    Note: this file *must* end with a .el extension. It will be byte-compiled
    after it is generated.

  `$DOOMPROFILELOADPATH'
    A colon-delimited (semi-colon on Windows) list of profile config files or
    directories under which Doom will search for implicit profiles. See
    `var:doom-profile-load-path' for its default value.

EXIT CODES:
  0        Successful run
  1        No error occurred, but the command couldn't complete
  2        General internal error
  3        Error with Emacs/Doom install or execution context
  4        Unrecognized user input error
  5        Command not found, or is incorrect/deprecated
  6        Invalid, missing, or extra options/arguments
  7-15     Reserved for Doom
  16-192   Reserved for the user's extensions
  254      Successful run (then execute the dynamically generated after-script)
  255      Uncaught critical errors

SEE ALSO:
  https://doomemacs.org               Homepage
  https://docs.doomemacs.org          Official documentation
  https://wiki.doomemacs.org          Community wiki
  https://doomemacs.org/discuss       Github discussions & user support board
  https://doomemacs.org/discord       Discord chat server"
  :partial t)

(defcli! :before
    ((force?   ("-!" "--force")   "Suppress prompts by auto-accepting their consequences")
     (debug?   ("-D" "--debug")   "Enable debug output")
     (verbose? ("-v" "--verbose") "Enable verbose output")
     (doomdir  ("--doomdir" dir)  "Use Doom config living in `DIR' (e.g. ~/.doom.d)")
     (emacsdir ("--emacsdir" dir) "Use Doom install living in `DIR' (e.g. ~/.emacs.d)")
     (pager    ("--pager" cmd)    "Pager command to use for large output")
     (profile  ("--profile" name) "Use profile named NAME")
     (bench?   ("--benchmark")    "Always display the benchmark")
     &flags
     (color?   ("--color") "Whether or not to show ANSI color codes")
     &multiple
     (loads    ("-L" "--load" "--strict-load" file) "Load elisp `FILE' before executing `COMMAND'")
     (evals    ("-E" "--eval" form) "Evaluate `FORM' before executing commands")
     &input input
     &context context
     &args _)
  "OPTIONS:
  -E, -eval
    Can be used multiple times.

  -L, --load, --strict-load
    Can be used multiple times to load multiple files. Both -L and --load will
    silently fail on missing files, but --strict-load won't.

    Warning: files loaded this way load too late to define new commands. To
    define commands, do so from `$DOOMDIR'/cli.el, `$DOOMDIR'/init.el, or a
    .doomrc file in the current project tree."
  (when bench?
    (setq doom-cli-benchmark-threshold 'always))
  (unless init-file-debug ; debug-mode implies verbose
    (when verbose?
      (setq doom-print-minimum-level 'info)))
  (when color?
    (setq doom-print-backend (if (eq color? :yes) 'ansi)))
  (when pager
    (setq doom-cli-pager pager))
  (when force?
    (setf (doom-cli-context-suppress-prompts-p context) t))
  ;; For these settings to take full effect, the script must be restarted:
  (when (or debug?
            profile
            emacsdir
            doomdir)
    (let (omit)
      (when debug?
        (setenv "DEBUG" "1")
        (setq init-file-debug t)
        (push "--debug" omit)
        (push "-D" omit))
      (when profile
        (setenv "DOOMPROFILE" profile)
        (push "--profile=" omit))
      (when emacsdir
        (setenv "EMACSDIR" emacsdir)
        (push "--emacsdir=" omit))
      (when doomdir
        (setenv "DOOMDIR" doomdir)
        (push "--doomdir=" omit))
      (exit! :restart :omit omit)))
  ;; Load extra files and forms, as per given options.
  (dolist (file loads)
    (load (doom-path (cdr file))
          (not (equal (car file) "--strict-load"))
          (not init-file-debug) t))
  (dolist (form evals)
    (eval (read (cdr form)) t)))


;;
;;; Load user config + commands

;; Load $DOOMDIR/init.el, to read the user's `doom!' block and give users an
;; opportunity to customize the CLI environment, if they like. Otherwise, they
;; can do so in .doomrc or .doomproject.
(doom-modules-initialize)

;; There are a lot of CLIs, and some have expensive initialization, so best we
;; load them lazily.
(defcli-group!
  :prefix 'doom
  (defcli-alias! ((version v)) (:root :version))
  ;; Import this for implicit 'X help' commands for your script:
  (defcli-alias! ((help h)) (:root :help))
  ;; And suggest its use when errors occur.
  (add-to-list 'doom-help-commands "%p h[elp] %c")

  (defcli-autoload! ((doctor doc)))
  (defcli-autoload! (info))

  (defcli-group! "Config Management"
    :docs "Commands for maintaining your Doom Emacs configuration."
    (defcli-autoload! ((sync s)))
    (defcli-autoload! ((profile pf)))
    (defcli-autoload! ((upgrade up)))
    (defcli-autoload! (env))
    (defcli-autoload! (gc))
    (defcli-autoload! (install))

    (defcli-obsolete! (profiles sync) (profile sync "--all") "3.0.0")
    (defcli-obsolete! ((build b)) (sync "--rebuild") "3.0.0")
    (defcli-obsolete! ((purge p)) (gc) "3.0.0")

    ;; TODO Post-3.0 commands
    (defcli-stub! module)
    (defcli-stub! nuke)
    (defcli-stub! package)
    (defcli-stub! rollback))

  (defcli-group! "Development"
    :docs "Commands for developing or launching Doom."
    (defcli-autoload! (ci))
    (defcli-autoload! (make))
    (defcli-autoload! (emacs))
    (defcli-obsolete! (run) (emacs) "3.0.0")

    ;; TODO Post-3.0 commands
    (defcli-stub! test))

  (let ((cli-file "cli.el"))
    (defcli-group! "Module commands"
      (with-doom-context 'module
        (dolist (key (doom-module-list))
          (when-let* ((path (doom-module-locate-path key cli-file)))
            (defcli-group! :prefix (if (cdr key) (format "+%s" (cdr key)))
              (doom-load (file-name-sans-extension path))))))))

  ;; Execute arbitrary CLI configuration in a local .doom.el file
  (let (doomrc)
    (cond
     ((setq doomrc (getenv "DOOMRC"))
      (load! doomrc default-directory))
     ((setq doomrc (locate-dominating-file default-directory ".doom.el"))
      (load! ".doom.el" doomrc)))))


;;
;;; Let 'er rip

(run! "doom" (cdr (member "--" argv)))

;;; doom ends here, unless...
