(provide 'em-smart)
(eval-when-compile (require 'esh-maint))
(defgroup eshell-smart nil
"This module combines the facility of normal, modern shells with
some of the edit/review concepts inherent in the design of Plan 9's
9term. See the docs for more details.
Most likely you will have to turn this option on and play around with
it to get a real sense of how it works."
:tag "Smart display of output"
:group 'eshell-module)
(defcustom eshell-smart-load-hook '(eshell-smart-initialize)
"*A list of functions to call when loading `eshell-smart'."
:type 'hook
:group 'eshell-smart)
(defcustom eshell-smart-unload-hook
(list
(function
(lambda ()
(remove-hook 'window-configuration-change-hook
'eshell-refresh-windows))))
"*A hook that gets run when `eshell-smart' is unloaded."
:type 'hook
:group 'eshell-smart)
(defcustom eshell-review-quick-commands nil
"*If t, always review commands.
Reviewing means keeping point on the text of the command that was just
invoked, to allow corrections to be made easily.
If set to nil, quick commands won't be reviewed. A quick command is a
command that produces no output, and exits successfully.
If set to `not-even-short-output', then the definition of \"quick
command\" is extended to include commands that produce output, iff
that output can be presented in its entirely in the Eshell window."
:type '(choice (const :tag "No" nil)
(const :tag "Yes" t)
(const :tag "Not even short output"
not-even-short-output))
:group 'eshell-smart)
(defcustom eshell-smart-display-navigate-list
'(insert-parentheses
mouse-yank-at-click
mouse-yank-secondary
yank-pop
yank-rectangle
yank)
"*A list of commands which cause Eshell to jump to the end of buffer."
:type '(repeat function)
:group 'eshell-smart)
(defcustom eshell-smart-space-goes-to-end t
"*If non-nil, space will go to end of buffer when point-max is visible.
That is, if a command is running and the user presses SPACE at a time
when the end of the buffer is visible, point will go to the end of the
buffer and smart-display will be turned off (that is, subsequently
pressing backspace will not cause the buffer to scroll down).
This feature is provided to make it very easy to watch the output of a
long-running command, such as make, where it's more desirable to see
the output go by than to review it afterward.
Setting this variable to nil means that space and backspace will
always have a consistent behavior, which is to move back and forth
through displayed output. But it also means that enabling output
tracking requires the user to manually move point to the end of the
buffer using \\[end-of-buffer]."
:type 'boolean
:group 'eshell-smart)
(defcustom eshell-where-to-jump 'begin
"*This variable indicates where point should jump to after a command.
The options are `begin', `after' or `end'."
:type '(radio (const :tag "Beginning of command" begin)
(const :tag "After command word" after)
(const :tag "End of command" end))
:group 'eshell-smart)
(defvar eshell-smart-displayed nil)
(defvar eshell-smart-command-done nil)
(defvar eshell-currently-handling-window nil)
(defun eshell-smart-initialize ()
"Setup Eshell smart display."
(unless eshell-non-interactive-p
(set (make-local-variable 'eshell-scroll-to-bottom-on-output) nil)
(set (make-local-variable 'eshell-scroll-to-bottom-on-input) nil)
(set (make-local-variable 'eshell-scroll-show-maximum-output) t)
(add-hook 'window-scroll-functions 'eshell-smart-scroll-window nil t)
(add-hook 'window-configuration-change-hook 'eshell-refresh-windows)
(add-hook 'eshell-output-filter-functions 'eshell-refresh-windows t t)
(add-hook 'after-change-functions 'eshell-disable-after-change nil t)
(add-hook 'eshell-input-filter-functions 'eshell-smart-display-setup nil t)
(make-local-variable 'eshell-smart-command-done)
(add-hook 'eshell-post-command-hook
(function
(lambda ()
(setq eshell-smart-command-done t))) t t)
(unless (eq eshell-review-quick-commands t)
(add-hook 'eshell-post-command-hook
'eshell-smart-maybe-jump-to-end nil t))))
(defun eshell-smart-scroll-window (wind start)
"Scroll the given Eshell window accordingly."
(unless eshell-currently-handling-window
(let ((inhibit-point-motion-hooks t)
(eshell-currently-handling-window t))
(save-selected-window
(select-window wind)
(eshell-smart-redisplay)))))
(defun eshell-refresh-windows (&optional frame)
"Refresh all visible Eshell buffers."
(let (affected)
(walk-windows
(function
(lambda (wind)
(with-current-buffer (window-buffer wind)
(if eshell-mode
(let (window-scroll-functions)
(eshell-smart-scroll-window wind (window-start))
(setq affected t))))))
0 frame)
(if affected
(let (window-scroll-functions)
(eshell-redisplay)))))
(defun eshell-smart-display-setup ()
"Set the point to somewhere in the beginning of the last command."
(cond
((eq eshell-where-to-jump 'begin)
(goto-char eshell-last-input-start))
((eq eshell-where-to-jump 'after)
(goto-char (next-single-property-change
eshell-last-input-start 'arg-end))
(if (= (point) (- eshell-last-input-end 2))
(forward-char)))
((eq eshell-where-to-jump 'end)
(goto-char (1- eshell-last-input-end)))
(t
(error "Invalid value for `eshell-where-to-jump'")))
(setq eshell-smart-command-done nil)
(add-hook 'pre-command-hook 'eshell-smart-display-move nil t)
(eshell-refresh-windows))
(defun eshell-disable-after-change (b e l)
"Disable smart display mode if the buffer changes in any way."
(when eshell-smart-command-done
(remove-hook 'pre-command-hook 'eshell-smart-display-move t)
(setq eshell-smart-command-done nil)))
(defun eshell-smart-maybe-jump-to-end ()
"Jump to the end of the input buffer.
This is done whenever a command exits successfully and both the command
and the end of the buffer are still visible."
(when (and (= eshell-last-command-status 0)
(if (eq eshell-review-quick-commands 'not-even-short-output)
(and (pos-visible-in-window-p (point-max))
(pos-visible-in-window-p eshell-last-input-start))
(= (count-lines eshell-last-input-end
eshell-last-output-end) 0)))
(goto-char (point-max))
(remove-hook 'pre-command-hook 'eshell-smart-display-move t)))
(defun eshell-smart-redisplay ()
"Display as much output as possible, smartly."
(if (eobp)
(save-excursion
(recenter -1)
(eshell-redisplay))
(let ((top-point (point)))
(and (memq 'eshell-smart-display-move pre-command-hook)
(>= (point) eshell-last-input-start)
(< (point) eshell-last-input-end)
(set-window-start (selected-window)
(line-beginning-position) t))
(if (pos-visible-in-window-p (point-max))
(save-excursion
(goto-char (point-max))
(recenter -1)
(unless (pos-visible-in-window-p top-point)
(goto-char top-point)
(set-window-start (selected-window)
(line-beginning-position) t)))))))
(defun eshell-smart-goto-end ()
"Like `end-of-buffer', but do not push a mark."
(interactive)
(goto-char (point-max)))
(defun eshell-smart-display-move ()
"Handle self-inserting or movement commands intelligently."
(let (clear)
(if (or current-prefix-arg
(and (> (point) eshell-last-input-start)
(< (point) eshell-last-input-end))
(>= (point) eshell-last-output-end))
(setq clear t)
(cond
((eq this-command 'self-insert-command)
(if (eq last-command-char ? )
(if (and eshell-smart-space-goes-to-end
eshell-current-command)
(if (not (pos-visible-in-window-p (point-max)))
(setq this-command 'scroll-up)
(setq this-command 'eshell-smart-goto-end))
(setq this-command 'scroll-up))
(setq clear t)
(goto-char (point-max))))
((eq this-command 'delete-backward-char)
(setq this-command 'ignore)
(if (< (point) eshell-last-input-start)
(eshell-show-output)
(if (pos-visible-in-window-p eshell-last-input-start)
(progn
(ignore-errors
(scroll-down))
(eshell-show-output))
(scroll-down)
(if (pos-visible-in-window-p eshell-last-input-end)
(eshell-show-output)))))
((or (memq this-command eshell-smart-display-navigate-list)
(and (eq this-command 'eshell-send-input)
(not (and (>= (point) eshell-last-input-start)
(< (point) eshell-last-input-end)))))
(setq clear t)
(goto-char (point-max)))))
(if clear
(remove-hook 'pre-command-hook 'eshell-smart-display-move t))))