;; View a file with "live" updates as the file grows on disk.
;; Copyright (C) 1991 Dave Gillespie

;; This file is part of GNU Emacs.

;; GNU Emacs is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY.  No author or distributor
;; accepts responsibility to anyone for the consequences of using it
;; or for whether it serves any particular purpose or works at all,
;; unless he says so in writing.  Refer to the GNU Emacs General Public
;; License for full details.

;; Everyone is granted permission to copy, modify and redistribute
;; GNU Emacs, but only under the conditions described in the
;; GNU Emacs General Public License.   A copy of this license is
;; supposed to have been given to you along with GNU Emacs so you
;; can know your rights and responsibilities.  It should be in a
;; file named COPYING.  Among other things, the copyright notice
;; and this notice must be preserved on all copies.

;; Usage:
;;   (autoload 'live-find-file "live" "View a file with \"tail -f\"" t)
;;   M-x live-find-file RET filename RET


(defvar live-file-notify t
  "*Non-nil to display a message in the echo area if data comes in on a
live file buffer which is not currently displayed.
If Non-nil and not \"t\", actually display the buffer if new data comes in.")

(defvar live-file-mask (concat "^\000\001\002\003\004\005\006\007\010\011"
			       "\013\015\016\017\020\021\022\023\024\025"
			       "\026\027\030\031\032\033\034\035\036\037\177")
  "*A string of characters to retain in a live file buffer;
all others are discarded.  If it begins with \"^\", it is a list of
characters which *should* be discarded.
Default is to discard all control characters except new-line and form-feed.")



(defun live-find-file (filename)
  "Read in and display a file with \"live\" updates as the file grows on disk.
This is accomplished using the Unix \"tail -f\" command.
The buffer will remain live until you kill it with \\[kill-buffer].
The buffer is made read-only, and should probably be left that way.
Inside the buffer, hit n to change notification mode, r to re-read, q to kill."
  (interactive "fLive find file: ")
  (setq filename (expand-file-name filename))
  (if (file-directory-p filename)
      (error "%s is a directory." filename))
  (let ((buf (create-file-buffer filename))
	size)
    (switch-to-buffer buf)
    (fundamental-mode)
    (setq major-mode 'live-find-file)
    (setq mode-name "Live-Find-File")
    (use-local-map (make-sparse-keymap))   ; should set up a real major mode!
    (local-set-key "q" "\C-xk\r")
    (local-set-key "n" 'Live-file-notify)
    (local-set-key "r" 'Live-file-reread)
    (local-set-key " " 'scroll-up)
    (local-set-key "\177" 'scroll-down)
    (local-set-key "?" 'Live-file-help)
    (local-set-key "h" 'Live-file-help)
    (erase-buffer)
    (insert-file-contents filename t)
    (setq size (buffer-size))
    (live-file-clean-region (point-min) (point-max))
    (set-buffer-modified-p nil)
    (setq buffer-read-only t)
    (goto-char (point-max))
    (setq default-directory (file-name-directory filename))
    (let* ((process-connection-type nil)
	   (proc (start-process
		  "tail" (current-buffer) "tail"
		  (format "+%dcf" (1+ size))
		  filename)))
      (process-kill-without-query proc)
      (set-process-filter proc 'live-file-filter)))
)

(defun Live-file-help ()
  (interactive)
  (message "Live mode:  n=change notification method, q=quit, r=reread")
)

(defun Live-file-notify (flag)
  "Control whether to notify on input from live files not visible in a window.
This applies to files created by \\[live-find-file].
Cycles among three options:  Notify in echo area, notify by creating a new
window, or don't notify.
Notification mode applies if input arrives for a live file that is not
currently visible.  If input arrives for a live file that is visible but
that has been scrolled so that the end is not visible, new input always
notifies in the echo area."
  (interactive "P")
  (setq live-file-notify
	(cond ((null flag)
	       (or (not live-file-notify)
		   (and (eq live-file-notify t)
			'show)))
	      ((not (integerp flag)) 'show)
	      ((> flag 0) t)))
  (message (cond ((eq live-file-notify t) "Notifying in echo area.")
		 (live-file-notify "Notifying in a new window.")
		 (t "Not notifying.")))
)

(defun Live-file-reread ()
  "Re-read the contents of the current live file from disk.
Live-find-file only checks for new text being appended at the end.  This
command re-reads the whole of the file to see if any of it has changed."
  (interactive)
  (let ((name (buffer-file-name)))
    (or name
	(error "Buffer is not visiting a file."))
    (or (file-readable-p name)
	(error "File %s no longer exists." name))
    (message "Re-reading %s..." name)
    (kill-buffer (current-buffer))
    (live-find-file name)
    (message "Re-reading %s...done" name))
)

(defun live-file-filter (proc str)
  (let* ((oldbuf (current-buffer))
	 (buf (process-buffer proc))
	 (win (get-buffer-window buf))
	 (changed nil))
    (unwind-protect
	(progn
	  (set-buffer buf)
	  (clear-visited-file-modtime)
	  (let ((pt (point-max))
		(opt (and (< (point) (point-max)) (point)))
		(mod (buffer-modified-p)))
	    (goto-char pt)
	    (let ((buffer-read-only nil))
	      (insert str)
	      (live-file-clean-region pt (point-max)))
	    (setq changed (/= pt (point-max)))
	    (and changed
		 (or (not win)
		     (not (pos-visible-in-window-p pt win)))
		 (or win (eq live-file-notify t))
		 (= (minibuffer-depth) 0)
		 (message "From %s: %s"
			  (file-name-nondirectory
			   (buffer-file-name buf))
			  str))
	    (or mod
		(set-buffer-modified-p nil))
	    (or opt (setq opt (point-max)))
	    (goto-char opt)
	    (and win (set-window-point win opt))))
      (set-buffer oldbuf))
    (and changed
	 live-file-notify 
	 (not (eq live-file-notify t))
	 (not (get-buffer-window buf))
	 (set-window-point (display-buffer buf t)
			   (save-excursion (set-buffer buf)
					   (goto-char (point-max))
					   (point)))))
)

(defun live-file-clean-region (start end)
  (and live-file-mask
       (save-excursion
	 (goto-char end)
	 (while (progn
		  (skip-chars-backward live-file-mask start)
		  (> (point) start))
	   (delete-backward-char 1))))
)