;;; multi-shell.el --- use escreen and term to manage multiple shells

;; Author: Mark Triggs <mst@dishevelled.net>

;; This file 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 2, or (at your option)
;; any later version.

;; This file 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 GNU Emacs; see the file COPYING.  If not, write to
;; the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
;; Boston, MA 02111-1307, USA.

;;; Commentary:


;;; Code:
(defvar multi-shell-screen nil "The escreen screen number used for shell")

(defun multi-shell-list ()
  "The shell buffers presently active"
  (sort
   (remove-if-not (lambda (b) (string-match "^\*terminal" (buffer-name b)))
                  (buffer-list))
   (lambda (a b)
     (< (string-to-number
         (cadr (split-string (buffer-name a) "[<>]")))
        (string-to-number
         (cadr (split-string (buffer-name b)  "[<>]")))))))


(defun multi-shell-next ()
  "Go to the next shell"
  (interactive)
  (multi-shell-switch 'NEXT))

(defun multi-shell-prev ()
  "Go to the previous shell"
  (interactive)
  (multi-shell-switch 'PREVIOUS))


(defun multi-shell-switch (direction)
  "if direction is NEXT, switch to the next shell. If PREVIOUS, switch to the
  previous shell"
  (let ((shells (multi-shell-list)))
    (setf (cdr (last shells)) shells)
    (let ((this-buffer (position (current-buffer) (multi-shell-list))))
      (if this-buffer
          (if (eql direction 'NEXT)
              (switch-to-buffer (nth (1+ this-buffer) shells))
            (switch-to-buffer (nth (+ (1- (length (multi-shell-list)))
                                      this-buffer) shells)))
        (switch-to-buffer (car shells))))))

(defvar multi-shell-dir nil)
(defun multi-shell ()
  (interactive)
  (let ((dir default-directory))
    (cond ((and multi-shell-screen
                (/= multi-shell-screen escreen-current-screen-number))
           ;; a shell screen exists, but is not focused
           (escreen-goto-screen multi-shell-screen)
           (unless (string-match "terminal" (buffer-name (current-buffer)))
             (if (null (multi-shell-list))
                 (multi-shell-new dir)
               (pop-to-buffer (car (multi-shell-list)))
               (setq multi-shell-dir dir))))

          (multi-shell-screen
           ;; a shell screen exists and is focused
           (if (or (string-match "terminal" (buffer-name (current-buffer)))
                   (null (multi-shell-list)))
               (multi-shell-new (if multi-shell-dir
                                    (prog1 multi-shell-dir
                                      (setq multi-shell-dir nil))
                                  dir))
             (switch-to-buffer (car (multi-shell-list)))))
          (t (escreen-create-screen)
             (setq multi-shell-screen escreen-current-screen-number)
             (multi-shell-new dir)))))

(defun multi-shell-new (&optional dir)
  (interactive)

  (let* ((nshells (length (multi-shell-list)))
         (n (if nshells (1+ nshells) 1))
         (term-term-name "vt100")
         (term-buffer
          (with-temp-buffer
            (if dir
                (cd dir)
              (cd (expand-file-name "~/")))
            (make-term (concat "terminal<" (format "%s" n) ">")
                       (or (getenv "SHELL") "bash")))))
    (set-buffer term-buffer)
    (term-mode)
    (term-char-mode)
    (when (ignore-errors (get-buffer-process (current-buffer)))
      (set-process-sentinel (get-buffer-process (current-buffer))
                            (lambda (proc change)
                              (when (or (string-match "finished" change)
                                        (string-match "exited" change))
                                (escreen-goto-screen multi-shell-screen)
                                (with-current-buffer (process-buffer proc)
                                  (multi-shell-prev))
                                (kill-buffer (process-buffer proc))))))

    (setq term-scroll-show-maximum-output nil)
    (setq term-scroll-to-bottom-on-output nil)
    (switch-to-buffer term-buffer)

    (define-key term-raw-map [(control prior)] 'multi-shell-prev)
    (define-key term-raw-map [(control next)] 'multi-shell-next)

    (define-key term-raw-map [(shift prior)] 'scroll-down)
    (define-key term-raw-map [(shift next)] 'scroll-up)

    (define-key term-raw-map [(control d)] 'term-delchar-or-maybe-eof-mst)
    ;; erc connection tracking
    (define-key term-raw-map (kbd "M-`")
      (lookup-key (current-global-map) (kbd "M-`")))))


(defun multi-shell-select-or-create ()
  "If the shell screen number is not selected, select it. Otherwise, run an
  new shell."
  (interactive)
  (if (or (not multi-shell-screen)
          (= escreen-current-screen-number multi-shell-screen))
      (multi-shell-new default-directory)
    (escreen-goto-screen multi-shell-screen)
    (cond ((null (multi-shell-list))
           (multi-shell-new (if (buffer-file-name)
                                (file-name-directory (buffer-file-name))
                              nil)))
          ((not (string-match "terminal" (buffer-name (current-buffer))))
           (switch-to-buffer (car (multi-shell-list)))))))

(defun term-delchar-or-maybe-eof-mst ()
  (interactive)

  (when (looking-at "\n")
    (delete-char 1))

  (cond ((eobp)
         (kill-process (get-buffer-process (current-buffer)))
         (kill-buffer nil))
        (t (term-send-raw-string "\C-d")
           (delete-char 1))))

(define-key global-map (kbd "C-c t") 'multi-shell)

(add-hook 'term-load-hook
          (lambda ()
            (interactive)

            (define-key term-mode-map [(control d)]
              'term-delchar-or-maybe-eof-mst)
            (define-key term-mode-map [(control prior)] 'multi-shell-prev)
            (define-key term-mode-map [(control next)] 'multi-shell-next)))

(provide 'multi-shell)
