;;; helm-external.el --- Run Externals commands within Emacs with helm completion. -*- lexical-binding: t -*-

;; Copyright (C) 2012 ~ 2020 Thierry Volpiatto <thierry.volpiatto@gmail.com>

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <http://www.gnu.org/licenses/>.

;;; Code:

(require 'cl-lib)
(require 'helm)
(require 'helm-help)
(require 'helm-net)

(declare-function helm-comp-read "helm-mode")


(defgroup helm-external nil
  "External related Applications and libraries for Helm."
  :group 'helm)

(defcustom helm-raise-command nil
  "A shell command to jump to a window running specific program.
Need external program wmctrl.
This will be use with `format', so use something like \"wmctrl -xa %s\"."
  :type 'string
  :group 'helm-external)

(defcustom helm-external-programs-associations nil
  "Alist to store externals programs associated with file extension.
This variable overhide setting in .mailcap file.
E.g.: '\(\(\"jpg\" . \"gqview\"\) (\"pdf\" . \"xpdf\"\)\) "
  :type '(alist :key-type string :value-type string)
  :group 'helm-external)

(defcustom helm-default-external-file-browser "nautilus"
  "Default external file browser for your system.
Directories will be opened externally with it when opening file
externally in `helm-find-files'.
Set to nil if you do not have an external file browser or do not
want to use it.
Windows users should set that to \"explorer.exe\"."
  :group 'helm-external
  :type  'string)


;;; Internals
(defvar helm-external-command-history nil)
(defvar helm-external-commands-list nil
  "A list of all external commands the user can execute.
If this variable is not set by the user, it will be calculated
automatically.")

(defun helm-external-commands-list-1 (&optional sort)
  "Returns a list of all external commands the user can execute.
If `helm-external-commands-list' is non-nil it will return its
contents.  Else it calculates all external commands and sets
`helm-external-commands-list'."
  (helm-aif helm-external-commands-list
      it
    (setq helm-external-commands-list
          (cl-loop
                for dir in (split-string (getenv "PATH") path-separator)
                when (and (file-exists-p dir) (file-accessible-directory-p dir))
                for lsdir = (cl-loop for i in (directory-files dir t)
                                  for bn = (file-name-nondirectory i)
                                  when (and (not (member bn completions))
                                            (not (file-directory-p i))
                                            (file-executable-p i))
                                  collect bn)
                append lsdir into completions
                finally return
                (if sort (sort completions 'string-lessp) completions)))))

(defun helm-run-or-raise (exe &optional file)
  "Run asynchronously EXE or jump to the application window.
If EXE is already running just jump to his window if
`helm-raise-command' is non-nil.
When FILE argument is provided run EXE with FILE."
  (let* ((real-com (car (split-string exe)))
         (proc     (if file (concat real-com " " file) real-com))
         process-connection-type)
    (if (get-process proc)
        (if helm-raise-command
            (shell-command  (format helm-raise-command real-com))
          (error "Error: %s is already running" real-com))
      (when (member real-com helm-external-commands-list)
        (message "Starting %s..." real-com)
        (if file
            (start-process-shell-command
             proc nil (format "%s %s"
                              real-com
                              (shell-quote-argument
                               (if (eq system-type 'windows-nt)
                                   (helm-w32-prepare-filename file)
                                   (expand-file-name file)))))
          (start-process-shell-command proc nil real-com))
        (set-process-sentinel
         (get-process proc)
         (lambda (process event)
             (when (and (string= event "finished\n")
                        helm-raise-command
                        (not (helm-get-pid-from-process-name real-com)))
               (shell-command  (format helm-raise-command "emacs")))
             (message "%s process...Finished." process))))
      (setq helm-external-commands-list
            (cons real-com
                  (delete real-com helm-external-commands-list))))))

(defun helm-get-mailcap-for-file (filename)
  "Get the command to use for FILENAME from mailcap files."
  (mailcap-parse-mailcaps)
  (let* ((ext  (file-name-extension filename))
         (mime (when ext (mailcap-extension-to-mime ext)))
         (result (when mime (mailcap-mime-info mime))))
    ;; If elisp file have no associations in .mailcap
    ;; `mailcap-maybe-eval' is returned, in this case just return nil.
    (when (stringp result) (helm-basename result))))

(defun helm-get-default-program-for-file (filename)
  "Try to find a default program to open FILENAME.
Try first in `helm-external-programs-associations' and then in
mailcap file.  If nothing found return nil."
  (let* ((ext      (file-name-extension filename))
         (def-prog (assoc-default ext helm-external-programs-associations)))
    (cond ((and def-prog (not (string= def-prog ""))) def-prog)
          ((and helm-default-external-file-browser (file-directory-p filename))
           helm-default-external-file-browser)
          (t (helm-get-mailcap-for-file filename)))))

(defun helm-open-file-externally (file)
  "Open FILE with an external program.
Try to guess which program to use with
`helm-get-default-program-for-file'.
If not found or a prefix arg is given query the user which tool
to use."
  (let* ((fname      (expand-file-name file))
         (collection (helm-external-commands-list-1 'sort))
         (def-prog   (helm-get-default-program-for-file fname))
         (program    (if (or helm-current-prefix-arg (not def-prog))
                         ;; Prefix arg or no default program.
                         (prog1
                             (helm-comp-read
                              "Program: " collection
                              :must-match t
                              :name "Open file Externally"
                              :del-input nil
                              :history helm-external-command-history)
                           ;; Always prompt to set this program as default.
                           (setq def-prog nil))
                         ;; No prefix arg or default program exists.
                         def-prog)))
    (unless (or def-prog ; Association exists, no need to record it.
                ;; Don't try to record non--filenames associations (e.g urls).
                (not (file-exists-p fname)))
      (when
          (y-or-n-p
           (format
            "Do you want to make `%s' the default program for this kind of files? "
            program))
        (helm-aif (assoc (file-name-extension fname)
                         helm-external-programs-associations)
            (setq helm-external-programs-associations
                  (delete it helm-external-programs-associations)))
        (push (cons (file-name-extension fname)
                    (helm-read-string
                     "Program (Add args maybe and confirm): " program))
              helm-external-programs-associations)
        (customize-save-variable 'helm-external-programs-associations
                                 helm-external-programs-associations)))
    (helm-run-or-raise program file)
    (setq helm-external-command-history
          (cons program
                (delete program
                        (cl-loop for i in helm-external-command-history
                              when (executable-find i) collect i))))))

;;;###autoload
(defun helm-run-external-command (program)
  "Preconfigured `helm' to run External PROGRAM asyncronously from Emacs.
If program is already running exit with error.
You can set your own list of commands with
`helm-external-commands-list'."
  (interactive (list
                (helm-comp-read
                 "RunProgram: "
                 (helm-external-commands-list-1 'sort)
                 :must-match t
                 :del-input nil
                 :name "External Commands"
                 :history helm-external-command-history)))
  (helm-run-or-raise program)
  (setq helm-external-command-history
        (cons program (delete program
                              (cl-loop for i in helm-external-command-history
                                    when (executable-find i) collect i)))))


(provide 'helm-external)

;;; helm-external ends here
