;;; syd-eshell.el -*- lexical-binding: t; -*-

(require 'ring)
(require 'cl-lib)

(defvar eshell-buffer-name "*eshell*")

(defvar syd-eshell-buffers (make-ring 25)
  "List of open eshell buffers.")

(defun syd-eshell-buffers ()
  "TODO"
  (ring-elements syd-eshell-buffers))

;;;###autoload
(defun syd-eshell-run-command (command &optional buffer)
  "TODO"
  (let ((buffer
         (or buffer
             (if (eq major-mode 'eshell-mode)
                 (current-buffer)
               (cl-find-if #'buffer-live-p (syd-eshell-buffers))))))
    (unless buffer
      (user-error "No living eshell buffers available"))
    (unless (buffer-live-p buffer)
      (user-error "Cannot operate on a dead buffer"))
    (with-current-buffer buffer
      (goto-char eshell-last-output-end)
      (goto-char (line-end-position))
      (insert command)
      (eshell-send-input nil t))))

;;;###autoload
(defun syd-eshell/toggle (arg &optional command)
  "Toggle eshell popup window."
  (interactive "P")
  (let ((eshell-buffer
         (get-buffer-create
          (format "*eshell-popup:%s*"
                  (if (bound-and-true-p persp-mode)
                      (safe-persp-name (get-current-persp))
                    "main"))))
        confirm-kill-processes
        current-prefix-arg)
    (when arg
      (when-let* ((win (get-buffer-window eshell-buffer)))
        (delete-window win))
      (when (buffer-live-p eshell-buffer)
        (with-current-buffer eshell-buffer
          (fundamental-mode)
          (erase-buffer))))
    (if-let* ((win (get-buffer-window eshell-buffer)))
        (let (confirm-kill-processes)
          (delete-window win)
          (ignore-errors (kill-buffer eshell-buffer)))
      (with-current-buffer eshell-buffer
        (syd-mark-buffer-as-real)
        (if (eq major-mode 'eshell-mode)
            (run-hooks 'eshell-mode-hook)
          (eshell-mode))
        (when command
          (syd-eshell-run-command command eshell-buffer)))
      (pop-to-buffer eshell-buffer)
      (when (bound-and-true-p evil-mode)
        (call-interactively #'evil-append-line)))))

(defun syd-eshell-adapt-bash-aliases ()
  "Very sloppily convert aliases defined in Bash to an Eshell alias file."
  (interactive)
  (save-window-excursion
    (let ((err-buf (generate-new-buffer "*adapt-bash-aliases-err*"))
          (result-buf (generate-new-buffer "*adapted-bash-aliases*")))
      (with-current-buffer result-buf
        (insert "# Automatically generated by syd-eshell-adapt-bash-aliases\n"))
      (with-temp-buffer
        ;; Aliases are only loaded when bash is in interactive mode.
        (shell-command "bash -ic alias" (current-buffer) err-buf)
        (goto-char (point-min))
        (while (re-search-forward
                (rx bol "alias " (group (+ alphanumeric)) "='"
                    (group (* (not ?\'))) "'")
                nil t)
          (let ((eshell-alias (format "alias %s %s $*\n"
                                      (match-string 1)
                                      (match-string 2))))
            (with-current-buffer result-buf
              (insert eshell-alias)))))
      (unless (= 0 (buffer-size err-buf))
        (message "Some errors occured whilst fetching Bash's aliases:")
        (message (with-current-buffer err-buf (buffer-string))))
      (kill-buffer err-buf)
      result-buf)))

(defun syd-eshell-C-d ()
  "Imitate the typical 'C-d' behaviour in other shells.  Quits Eshell when the input is empty."
  (interactive)
  (when (and (eolp) (looking-back eshell-prompt-regexp nil))
    (eshell-life-is-too-much)))

(defun syd-eshell--init-ui-hacks ()
  (defun syd-eshell-remove-fringes-h ()
    (set-window-fringes nil 0 0)
    (set-window-margins nil 1 nil))
  (defun syd-eshell-enable-text-wrapping-h ()
    (visual-line-mode +1)
    (set-display-table-slot standard-display-table 0 ?\ ))
  (add-hook 'eshell-mode-hook #'syd-eshell-remove-fringes-h)
  (add-hook 'eshell-mode-hook #'syd-eshell-enable-text-wrapping-h)

  (with-eval-after-load 'hide-mode-line
    (add-hook 'eshell-mode-hook #'hide-mode-line-mode))
  
  ;; Remove hscroll-margin in shells, otherwise you get jumpiness when the
  ;; cursor comes close to the left/right edges of the window.
  (defun syd-eshell-disable-hscroll-margin ()
    (setq hscroll-margin 0))
  (add-hook 'eshell-mode-hook #'syd-eshell-disable-hscroll-margin))

(use-package shrink-path
  :defer t)

(defface syd-eshell-local-name '((t (:inherit font-lock-constant-face)))
  "Face used by the Eshell prompt for the CWD's non-TRAMP part.  See
`syd-eshell--prompt-fn'"
  :group 'eshell)

(defface syd-eshell-tramp-prefix '((t (:inherit font-lock-comment-face)))
  "Face used by the Eshell prompt for the CWD's TRAMP prefix.  See
`syd-eshell--prompt-fn'."
  :group 'eshell)

(defun syd-eshell--prompt-fn ()
  "See `eshell-prompt-function'."
  (require 'shrink-path)
  (require 'syd-file)
  (-let (((tramp-prefix . local-name) (syd-split-tramp-file-name (eshell/pwd))))
    (concat (unless (bobp) "\n")
            (when tramp-prefix
                (propertize tramp-prefix 'face 'syd-eshell-tramp-prefix))
            (propertize (if (equal local-name "~")
                            local-name
                          (abbreviate-file-name (shrink-path-file local-name)))
                        'face 'syd-eshell-local-name)
            (propertize " $"
                        'face (if (zerop eshell-last-command-status)
                                  'success
                                'error))
            " ")))

(defvar syd-eshell--prompt-regexp (rx bol (* (not (any "\n$"))) " $ "))

(set-popup-rule! "^\\*eshell-popup"
  :vslot -5 :size 13 :select t :modeline nil :quit nil :ttl nil)

(use-package eshell
  :init
  (defvar syd-eshell-data-dir (file-name-concat syd-data-dir
                                                "eshell"))
  (make-directory syd-eshell-data-dir t)
  :custom
  ((eshell-banner-message
    '(format "🦌 %s %s }:3\n"
      (propertize (format " %s " (string-trim (buffer-name)))
                  'face 'mode-line-highlight)
      (propertize (current-time-string)
                  'face 'font-lock-keyword-face)))
   (eshell-scroll-to-bottom-on-input 'all)
   (eshell-scroll-to-bottom-on-output nil)
   (eshell-kill-processes-on-exit t)
   (eshell-hist-ignoredups t)
   (eshell-glob-case-insensitive t)
   (eshell-error-if-no-glob t)
   (eshell-history-file-name (file-name-concat
                              syd-eshell-data-dir "history"))
   (eshell-last-dir-ring-file-name (file-name-concat
                                    syd-eshell-data-dir "lastdir"))
   (eshell-prompt-function #'syd-eshell--prompt-fn)
   (eshell-prompt-regexp syd-eshell--prompt-regexp))
  :general
  (:keymaps 'syd-leader-open-map
            "e" #'syd-eshell/toggle)
  (:keymaps 'eshell-mode-map
   :states 'insert
   "C-d" #'syd-eshell-C-d)
  (:keymaps 'eshell-mode-map
   :states 'motion
   "[ [" #'eshell-previous-prompt
   "] ]" #'eshell-next-prompt)
  (:keymaps 'eshell-mode-map
   :states '(normal insert)
   "C-j" #'eshell-next-matching-input-from-input
   "C-k" #'eshell-previous-matching-input-from-input
   "C-s" #'consult-history)
  :config
  ;; When cd'd into a TRAMP remote, automatically expand '/' to the TRAMP
  ;; notation referring to the remote's root.
  (add-to-list 'eshell-modules-list 'eshell-elecslash)

  (require 'syd-buffers)

  (add-hook 'eshell-mode-hook #'syd-mark-buffer-as-real)

  ;; UI enhancements.
  (syd-eshell--init-ui-hacks))

(provide 'syd-eshell)
 ;;; syd-eshell.el -*- lexical-binding: t; -*-
 
 (require 'ring)
 (require 'cl-lib)
 
 (defvar eshell-buffer-name "*eshell*")
 
 (defvar syd-eshell-buffers (make-ring 25)
   "List of open eshell buffers.")
 
 (defun syd-eshell-buffers ()
   "TODO"
   (ring-elements syd-eshell-buffers))
 
 ;;;###autoload
 (defun syd-eshell-run-command (command &optional buffer)
   "TODO"
   (let ((buffer
          (or buffer
              (if (eq major-mode 'eshell-mode)
                  (current-buffer)
                (cl-find-if #'buffer-live-p (syd-eshell-buffers))))))
     (unless buffer
       (user-error "No living eshell buffers available"))
     (unless (buffer-live-p buffer)
       (user-error "Cannot operate on a dead buffer"))
     (with-current-buffer buffer
       (goto-char eshell-last-output-end)
       (goto-char (line-end-position))
       (insert command)
       (eshell-send-input nil t))))
 
 ;;;###autoload
 (defun syd-eshell/toggle (arg &optional command)
   "Toggle eshell popup window."
   (interactive "P")
   (let ((eshell-buffer
          (get-buffer-create
           (format "*eshell-popup:%s*"
                   (if (bound-and-true-p persp-mode)
                       (safe-persp-name (get-current-persp))
                     "main"))))
         confirm-kill-processes
         current-prefix-arg)
     (when arg
       (when-let* ((win (get-buffer-window eshell-buffer)))
         (delete-window win))
       (when (buffer-live-p eshell-buffer)
         (with-current-buffer eshell-buffer
           (fundamental-mode)
           (erase-buffer))))
     (if-let* ((win (get-buffer-window eshell-buffer)))
         (let (confirm-kill-processes)
           (delete-window win)
           (ignore-errors (kill-buffer eshell-buffer)))
       (with-current-buffer eshell-buffer
         (syd-mark-buffer-as-real)
         (if (eq major-mode 'eshell-mode)
             (run-hooks 'eshell-mode-hook)
           (eshell-mode))
         (when command
           (syd-eshell-run-command command eshell-buffer)))
       (pop-to-buffer eshell-buffer)
       (when (bound-and-true-p evil-mode)
         (call-interactively #'evil-append-line)))))
 
 (defun syd-eshell (&optional arg)
   (interactive "P")
   (eshell (or arg t)))
 
 (defun syd-eshell-adapt-bash-aliases ()
   "Very sloppily convert aliases defined in Bash to an Eshell alias file."
   (interactive)
   (save-window-excursion
     (let ((err-buf (generate-new-buffer "*adapt-bash-aliases-err*"))
           (result-buf (generate-new-buffer "*adapted-bash-aliases*")))
       (with-current-buffer result-buf
         (insert "# Automatically generated by syd-eshell-adapt-bash-aliases\n"))
       (with-temp-buffer
         ;; Aliases are only loaded when bash is in interactive mode.
         (shell-command "bash -ic alias" (current-buffer) err-buf)
         (goto-char (point-min))
         (while (re-search-forward
                 (rx bol "alias " (group (+ alphanumeric)) "='"
                     (group (* (not ?\'))) "'")
                 nil t)
           (let ((eshell-alias (format "alias %s %s $*\n"
                                       (match-string 1)
                                       (match-string 2))))
             (with-current-buffer result-buf
               (insert eshell-alias)))))
       (unless (= 0 (buffer-size err-buf))
         (message "Some errors occured whilst fetching Bash's aliases:")
         (message (with-current-buffer err-buf (buffer-string))))
       (kill-buffer err-buf)
       result-buf)))
 
 (defun syd-eshell-C-d ()
   "Imitate the typical 'C-d' behaviour in other shells.  Quits Eshell when the input is empty."
   (interactive)
   (when (and (eolp) (looking-back eshell-prompt-regexp nil))
     (eshell-life-is-too-much)))
 
 (defun syd-eshell--init-ui-hacks ()
   (defun syd-eshell-remove-fringes-h ()
     (set-window-fringes nil 0 0)
     (set-window-margins nil 1 nil))
   (defun syd-eshell-enable-text-wrapping-h ()
     (visual-line-mode +1)
     (set-display-table-slot standard-display-table 0 ?\ ))
   (add-hook 'eshell-mode-hook #'syd-eshell-remove-fringes-h)
   (add-hook 'eshell-mode-hook #'syd-eshell-enable-text-wrapping-h)
 
   (with-eval-after-load 'hide-mode-line
     (add-hook 'eshell-mode-hook #'hide-mode-line-mode))
   
   ;; Remove hscroll-margin in shells, otherwise you get jumpiness when the
   ;; cursor comes close to the left/right edges of the window.
   (defun syd-eshell-disable-hscroll-margin ()
     (setq hscroll-margin 0))
   (add-hook 'eshell-mode-hook #'syd-eshell-disable-hscroll-margin))
 
 (use-package shrink-path
   :defer t)
 
 (defface syd-eshell-local-name '((t (:inherit font-lock-constant-face)))
   "Face used by the Eshell prompt for the CWD's non-TRAMP part.  See
 `syd-eshell--prompt-fn'"
   :group 'eshell)
 
 (defface syd-eshell-tramp-prefix '((t (:inherit font-lock-comment-face)))
   "Face used by the Eshell prompt for the CWD's TRAMP prefix.  See
 `syd-eshell--prompt-fn'."
   :group 'eshell)
 
 (defun syd-eshell--prompt-fn ()
   "See `eshell-prompt-function'."
   (require 'shrink-path)
   (require 'syd-file)
   (-let (((tramp-prefix . local-name) (syd-split-tramp-file-name (eshell/pwd))))
     (concat (unless (bobp) "\n")
             (when tramp-prefix
                 (propertize tramp-prefix 'face 'syd-eshell-tramp-prefix))
             (propertize (if (equal local-name "~")
                             local-name
                           (abbreviate-file-name (shrink-path-file local-name)))
                         'face 'syd-eshell-local-name)
             (propertize " $"
                         'face (if (zerop eshell-last-command-status)
                                   'success
                                 'error))
             " ")))
 
 (defvar syd-eshell--prompt-regexp (rx bol (* (not (any "\n$"))) " $ "))
 
 (set-popup-rule! "^\\*eshell-popup"
   :vslot -5 :size 13 :select t :modeline nil :quit nil :ttl nil)
 
 (use-package eshell
   :init
   (defvar syd-eshell-data-dir (file-name-concat syd-data-dir
                                                 "eshell"))
   (make-directory syd-eshell-data-dir t)
   :custom
   ((eshell-banner-message
     '(format "🦌 %s %s }:3\n"
       (propertize (format " %s " (string-trim (buffer-name)))
                   'face 'mode-line-highlight)
       (propertize (current-time-string)
                   'face 'font-lock-keyword-face)))
    (eshell-scroll-to-bottom-on-input 'all)
    (eshell-scroll-to-bottom-on-output nil)
    (eshell-kill-processes-on-exit t)
    (eshell-hist-ignoredups t)
    (eshell-glob-case-insensitive t)
    (eshell-error-if-no-glob t)
    (eshell-history-file-name (file-name-concat
                               syd-eshell-data-dir "history"))
    (eshell-last-dir-ring-file-name (file-name-concat
                                     syd-eshell-data-dir "lastdir"))
    (eshell-prompt-function #'syd-eshell--prompt-fn)
    (eshell-prompt-regexp syd-eshell--prompt-regexp))
   :general
   (:keymaps 'syd-leader-open-map
    "e" #'syd-eshell/toggle
    "E" #'syd-eshell)
   (:keymaps 'eshell-mode-map
    :states 'insert
    "C-d" #'syd-eshell-C-d)
   (:keymaps 'eshell-mode-map
    :states 'motion
    "[ [" #'eshell-previous-prompt
    "] ]" #'eshell-next-prompt)
   (:keymaps 'eshell-mode-map
    :states '(normal insert)
    "C-j" #'eshell-next-matching-input-from-input
    "C-k" #'eshell-previous-matching-input-from-input
    "C-s" #'consult-history)
   :config
   ;; When cd'd into a TRAMP remote, automatically expand '/' to the TRAMP
   ;; notation referring to the remote's root.
   (add-to-list 'eshell-modules-list 'eshell-elecslash)
 
   (require 'syd-buffers)
 
   (add-hook 'eshell-mode-hook #'syd-mark-buffer-as-real)
 
   ;; UI enhancements.
   (syd-eshell--init-ui-hacks))
 
 (provide 'syd-eshell)

