Add ffs (form feed slides) mode for GNU Emacs
authorAmin Bandali <bandali@kelar.org>
Fri, 20 May 2022 01:57:45 +0000 (21:57 -0400)
committerAmin Bandali <bandali@kelar.org>
Fri, 20 May 2022 02:20:05 +0000 (22:20 -0400)
This is what I used for preparing and  presenting my LibrePlanet 2022
talk, 'The Net beyond the web' back in March. :)

.emacs.d/init.el
.emacs.d/lisp/ffs/ChangeLog [new file with mode: 0644]
.emacs.d/lisp/ffs/ffs.el [new file with mode: 0644]
.emacs.d/lisp/ffs/ffsanim.el [new file with mode: 0644]

index ce05ab4..11e4fb2 100644 (file)
@@ -954,6 +954,21 @@ Effectively a very simple light/dark theme toggle switch."
 (with-eval-after-load 'emms
   (setq emms-directory (b/var "emms")))
 
+(add-to-list 'load-path (b/lisp "ffs"))
+(run-with-idle-timer 0.5 nil #'require 'ffs)
+(with-eval-after-load 'ffs
+  (global-set-key (kbd "C-c f s") #'ffs))
+
+(defun b/export-frame ()
+  (interactive)
+  ;; TODO: ask for fn and/or take as arg
+  (let* ((fn (make-temp-file "emacs" nil ".pdf"))
+         (data (x-export-frames nil 'pdf)))
+    (with-temp-file fn
+      (insert data))
+    (kill-new fn)
+    (message fn)))
+
 \f
 ;;; Post initialization
 
diff --git a/.emacs.d/lisp/ffs/ChangeLog b/.emacs.d/lisp/ffs/ChangeLog
new file mode 100644 (file)
index 0000000..41d87ca
--- /dev/null
@@ -0,0 +1,28 @@
+2022-03-09  Amin Bandali  <bandali@gnu.org>
+
+       * ffsanim.el: Rename from ffs.el to ffsanim.el.  Revision 0.1.5.
+
+2022-03-06  Amin Bandali  <bandali@gnu.org>
+
+       * ffs.el (ffs-new-slide): Merge function into `ffs-edit-slide'.
+       Revision 0.1.4.
+
+2022-03-05  Amin Bandali  <bandali@gnu.org>
+
+       * ffs.el: Implement insertion of new slide before or after the
+       current slide.  Revision 0.1.3.
+
+2022-03-05  Amin Bandali  <bandali@gnu.org>
+
+       * ffs.el (ffs-edit-mode): Change to a minor mode, and use the
+       source buffer's `major-mode' for the edit buffer.  Revision 0.1.2.
+
+2022-03-05  Amin Bandali  <bandali@gnu.org>
+
+       * ffs.el: Add `ffs-edit-mode' (derived from `text-mode') for the
+       new ffs-edit buffer for editing slides.  Revision 0.1.1.
+
+2022-03-04  Amin Bandali  <bandali@gnu.org>
+
+       * ffs.el: Initial revision 0.1.0.
+
diff --git a/.emacs.d/lisp/ffs/ffs.el b/.emacs.d/lisp/ffs/ffs.el
new file mode 100644 (file)
index 0000000..99f2097
--- /dev/null
@@ -0,0 +1,419 @@
+;;; ffs.el --- Form Feed Slides mode       -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2022  Amin Bandali <bandali@gnu.org>
+
+;; Author: Amin Bandali <bandali@gnu.org>
+;; Version: 0.1.0
+;; Keywords: outlines, tools
+
+;; 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 <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; A simple mode for doing simple plain text presentations where the
+;; slides are separated using the form feed character (\f).
+
+;; Configuration: TODO
+
+;; Usage:
+
+;; Put this file, ffs.el, in a directory in your `load-path', then add
+;; something like the following to your init file:
+;;
+;; (require 'ffs)
+;; (global-set-key (kbd "C-c f s") #'ffs)
+
+;; Then, open a text file/buffer that you would like you to use as the
+;; source of your presentation and type `M-x ffs RET' or a keyboard
+;; shortcut you defined (like the above example) to start ffs, at
+;; which point you should be able to see "ffs" appear as one of the
+;; currently enabled minor modes in your mode-line.  Once ffs is
+;; enabled, you can invoke its various commands.  To see a list of
+;; available commands, you can either type `M-x ffs- TAB' (to get a
+;; completion of commands starting with the "ffs-" prefix), or see the
+;; definition of `ffs-minor-mode-map' near the end of this file.
+
+;;; Code:
+
+(defgroup ffs nil
+  "Minor mode for form feed-separated plain text presentations."
+  :version "29.1"
+  :prefix "ffs-")
+
+(defcustom ffs-default-face-height 370
+  "The value of the `height' property for the `default' face to use
+during the ffs presentation."
+  :group 'ffs
+  :type '(choice (const nil)
+                 (integer :value 300)))
+
+(defcustom ffs-edit-buffer-name "*ffs-edit*"
+  "The name of the ffs-edit buffer used when editing a slide."
+  :group 'ffs
+  :type 'string)
+
+(defvar ffs--slides-buffer nil
+  "The main ffs presentation slides buffer.
+When the user enables ffs in a buffer using `\\[ffs]', we store a
+reference to that buffer in this variable.
+
+As a special case, in a speaker notes buffer selected by the user
+using `\\[ffs-find-speaker-notes-file]' from the main ffs slides
+buffer, this variable will point to the main ffs slides buffer
+rather than the speaker notes buffer.")
+
+(defvar ffs--notes-buffer nil
+  "The ffs speaker notes buffer (only if selected).
+When the user chooses (and opens) a speaker notes file using
+`\\[ffs-find-speaker-notes-file]', a reference to the file's
+corresponding buffer is stored in this variable, local to the
+main ffs presentation slides buffer (`ffs--slides-buffer').")
+
+(defvar ffs--old-mode-line-format nil
+  "The old value of `mode-line-format' before enabling
+`ffs--no-mode-line-minor-mode'.")
+
+(defvar ffs--old-cursor-type nil
+  "The old value of `cursor-type' before enabling
+`ffs--no-cursor-minor-mode'.")
+
+(defvar ffs--old-default-face-height nil
+  "The old value of the `default' face's `height' property before
+starting the ffs presentation.")
+
+(define-minor-mode ffs--no-mode-line-minor-mode
+  "Minor mode for hiding the mode-line."
+  :lighter nil
+  (if ffs--no-mode-line-minor-mode
+      (progn
+        (unless ffs--old-mode-line-format
+          (setq-local ffs--old-mode-line-format mode-line-format))
+        (setq-local mode-line-format nil))
+    (setq-local mode-line-format ffs--old-mode-line-format)
+    (when ffs--old-mode-line-format
+      ffs--old-mode-line-format nil))
+  (redraw-display))
+
+(define-minor-mode ffs--no-cursor-minor-mode
+  "Minor mode for hiding the cursor."
+  :lighter nil
+  (if ffs--no-cursor-minor-mode
+      (progn
+        (unless ffs--old-cursor-type
+          (setq-local ffs--old-cursor-type cursor-type))
+        (setq-local cursor-type nil))
+    (setq-local cursor-type ffs--old-cursor-type)
+    (when ffs--old-cursor-type
+      ffs--old-cursor-type nil)))
+
+(defun ffs--toggle-dark-mode ()
+  "Swap the frame background and foreground colours."
+  (interactive)
+  (let ((bg (frame-parameter nil 'background-color))
+        (fg (frame-parameter nil 'foreground-color)))
+    (set-background-color fg)
+    (set-foreground-color bg)))
+
+(defun ffs--goto-previous (buffer)
+  "Go to the previous slide in the given BUFFER."
+  (interactive)
+  (with-current-buffer buffer
+    (let ((n (buffer-narrowed-p)))
+      (when n
+        (goto-char (point-min))
+        (widen)
+        (backward-page))
+      (backward-page)
+      (when n (narrow-to-page)))))
+
+(defun ffs-goto-previous ()
+  "Go to the previous slide in the main ffs presentation and the
+speaker notes buffer (if any)."
+  (interactive)
+  (ffs--goto-previous ffs--slides-buffer)
+  (when ffs--notes-buffer
+    (ffs--goto-previous ffs--notes-buffer)
+    (redraw-display)))
+
+(defun ffs--goto-next (buffer)
+  "Go to the next slide in the given BUFFER."
+  (interactive)
+  (with-current-buffer buffer
+    (let ((n (buffer-narrowed-p))
+          (e (= (- (point-max) (point-min)) 0)))
+      (when n
+        (goto-char (point-min))
+        (widen))
+      (unless e (forward-page))
+      (when n (narrow-to-page)))))
+
+(defun ffs-goto-next ()
+  "Go to the next slide in the main ffs presentation and the
+speaker notes buffer (if any)."
+  (interactive)
+  (ffs--goto-next ffs--slides-buffer)
+  (when ffs--notes-buffer
+    (ffs--goto-next ffs--notes-buffer)
+    (redraw-display)))
+
+(defun ffs--goto-first (buffer)
+  "Go to the first slide in the given BUFFER."
+  (interactive)
+  (with-current-buffer buffer
+    (let ((n (buffer-narrowed-p)))
+      (when n (widen))
+      (goto-char (point-min))
+      (when n (narrow-to-page)))))
+
+(defun ffs-goto-first ()
+  "Go to the first slide in the main ffs presentation and the
+speaker notes buffer (if any)."
+  (interactive)
+  (ffs--goto-first ffs--slides-buffer)
+  (when ffs--notes-buffer
+    (ffs--goto-first ffs--notes-buffer)
+    (redraw-display)))
+
+(defun ffs--goto-last (buffer)
+  "Go to the last slide in the given BUFFER."
+  (interactive)
+  (let ((n (buffer-narrowed-p)))
+    (when n (widen))
+    (goto-char (point-max))
+    (when n (narrow-to-page))))
+
+(defun ffs-goto-last ()
+  "Go to the last slide in the main ffs presentation and the
+speaker notes buffer (if any)."
+  (interactive)
+  (ffs--goto-last ffs--slides-buffer)
+  (when ffs--notes-buffer
+    (ffs--goto-last ffs--notes-buffer)
+    (redraw-display)))
+
+(defun ffs-start ()
+  "Start the presentation."
+  (interactive)
+  (ffs-minor-mode 1)
+  (ffs--no-mode-line-minor-mode 1)
+  (ffs--no-cursor-minor-mode 1)
+  (when (integerp ffs-default-face-height)
+    (setq-local
+     ffs--old-default-face-height
+     (face-attribute 'default :height))
+    (face-remap-add-relative
+     'default :height ffs-default-face-height))
+  (show-paren-local-mode -1)
+  (display-battery-mode -1)
+  (flyspell-mode -1)
+  (narrow-to-page))
+
+(defun ffs-quit ()
+  "Quit the presentation."
+  (interactive)
+  (let ((n (buffer-narrowed-p))
+        (e (= (- (point-max) (point-min)) 0)))
+    (when (integerp ffs-default-face-height)
+      (face-remap-add-relative
+       'default :height ffs--old-default-face-height))
+    (show-paren-local-mode 1)
+    (display-battery-mode 1)
+    (flyspell-mode 1)
+    (ffs--no-mode-line-minor-mode -1)
+    (ffs--no-cursor-minor-mode -1)
+    (if n
+        (progn
+          (goto-char (point-min))
+          (widen))
+      (ffs-minor-mode -1))
+    (when e (forward-char -1))))
+
+(defun ffs-edit (&optional add-above-or-below)
+  "Pop to a new buffer to edit a slide.
+If ADD-ABOVE-OR-BELOW is nil or not given, we are editing an
+existing slide.  Otherwise, if it is `add-above' then the new
+slide will be added above/before the current slide, and if it is
+`add-below' then the new slide will be added below/after the
+current slide.  The logic is implemented in `ffs-edit-done'."
+  (interactive)
+  (let* ((b (current-buffer))
+         (m major-mode)
+         (n (buffer-narrowed-p))
+         (s (if add-above-or-below      ; if we are adding a new slide
+                "\n"                    ; start with just a newline
+              (unless n (narrow-to-page))
+              (prog1 (buffer-string)
+                (unless n (widen))))))
+    (pop-to-buffer-same-window
+     (get-buffer-create ffs-edit-buffer-name))
+    (funcall m)
+    (ffs-edit-minor-mode 1)
+    (insert s)
+    (goto-char (point-min))
+    (set-buffer-modified-p nil)
+    (setq-local
+     ffs--edit-source-buffer b
+     ffs--new-location add-above-or-below)
+    (message
+     (substitute-command-keys "Edit, then use `\\[ffs-edit-done]' \
+to apply your changes or `\\[ffs-edit-discard]' to discard them."))))
+
+(defun ffs-new-above ()
+  "Add a new slide above/before the current slide."
+  (interactive)
+  (ffs-edit 'add-above))
+
+(defun ffs-new-below ()
+  "Add a new slide below/after the current slide."
+  (interactive)
+  (ffs-edit 'add-below))
+
+(defun ffs-edit-discard ()
+  "Discard current ffs-edit buffer and return to the presentation."
+  (interactive)
+  (let ((b (current-buffer)))
+    (quit-windows-on b)
+    (kill-buffer b)))
+
+(defun ffs-edit-done ()
+  "Apply the ffs-edit changes and return to the presentation."
+  (interactive)
+  (let* (f
+         (str (buffer-string))
+         (s (if (string-suffix-p "\n" str)
+                str
+              (concat str "\n")))
+         (l ffs--new-location))
+    (with-current-buffer ffs--edit-source-buffer
+      (let ((inhibit-read-only t))
+        (save-excursion
+          (cond
+           ((eq l 'add-above)
+            (backward-page)
+            (insert (format "\n%s\f" s))
+            (setq f #'ffs-previous-slide))
+           ((eq l 'add-below)
+            (forward-page)
+            (insert (format "\n%s\f" s))
+            (setq f #'ffs-next-slide))
+           ((null l)
+            (narrow-to-page)
+            (delete-region (point-min) (point-max))
+            (insert s)
+            (widen))))))
+    (ffs-edit-discard)
+    (when (functionp f)
+      (funcall f))))
+
+(defun ffs--undo (&optional arg)
+  "Like `undo', but it works even when the buffer is read-only."
+  (interactive "P")
+  (let ((inhibit-read-only t))
+    (undo arg)))
+
+(defun ffs-find-speaker-notes-file (file)
+  "Prompt user for a speaker notes file, open it in a new frame."
+  (interactive "Fspeakers notes buffer: ")
+  (let ((b (current-buffer)))
+    (save-excursion
+      (find-file-other-frame file)
+      (ffs-minor-mode 1)
+      (setq-local
+       ffs--slides-buffer b
+       ffs--notes-buffer (current-buffer)))
+    (setq-local ffs--notes-buffer (get-file-buffer file))))
+
+(defun ffs-export-slides-to-pdf ()
+  (interactive)
+  (with-current-buffer ffs--slides-buffer
+    (ffs-goto-first)
+    (let ((c 1)
+          (fringe fringe-mode))
+      (fringe-mode 0)
+      (while (not (eobp))
+        (let ((fn (format "%s-%03d.pdf"
+                          (file-name-sans-extension (buffer-name))
+                          c))
+              (data (x-export-frames nil 'pdf)))
+          (with-temp-file fn
+            (insert data)))
+        (setq c (+ c 1))
+        (ffs-goto-next))
+      (fringe-mode fringe))))
+
+(defvar ffs-edit-minor-mode-map
+  (let ((map (make-sparse-keymap)))
+    (define-key map (kbd "C-c C-k") #'ffs-edit-discard)
+    (define-key map (kbd "C-c C-c") #'ffs-edit-done)
+    map)
+  "Keymap for `ffs-edit-minor-mode'.")
+
+(define-minor-mode ffs-edit-minor-mode
+  "Minor mode for editing a single ffs slide.
+When done editing the slide, run \\[ffs-edit-done] to apply your
+changes, or \\[ffs-edit-discard] to discard them."
+  :group 'ffs
+  :lighter " ffs-edit"
+  :keymap ffs-edit-minor-mode-map
+  (defvar-local ffs--edit-source-buffer nil
+    "The ffs presentation buffer of the slide being edited.")
+  (defvar-local ffs--new-location nil
+    "The location where the new slide should be inserted.
+See the docstring for `ffs-edit' for more details."))
+
+(defvar ffs-minor-mode-map
+  (let ((map (make-sparse-keymap)))
+    (define-key map (kbd "p") #'ffs-goto-previous)
+    (define-key map (kbd "n") #'ffs-goto-next)
+    (define-key map (kbd "DEL") #'ffs-goto-previous)
+    (define-key map (kbd "SPC") #'ffs-goto-next)
+    (define-key map (kbd "[") #'ffs-goto-previous)
+    (define-key map (kbd "]") #'ffs-goto-next)
+    (define-key map (kbd "<") #'ffs-goto-first)
+    (define-key map (kbd ">") #'ffs-goto-last)
+    (define-key map (kbd "s") #'ffs-start)
+    (define-key map (kbd "q") #'ffs-quit)
+    (define-key map (kbd "e") #'ffs-edit)
+    (define-key map (kbd "O") #'ffs-new-above)
+    (define-key map (kbd "o") #'ffs-new-below)
+    (define-key map (kbd "m") #'ffs--no-mode-line-minor-mode)
+    (define-key map (kbd "c") #'ffs--no-cursor-minor-mode)
+    (define-key map (kbd "d") #'ffs--toggle-dark-mode)
+    (define-key map (kbd "N") #'narrow-to-page)
+    (define-key map (kbd "W") #'widen)
+    (define-key map [remap undo] #'ffs--undo)
+    (define-key map (kbd "C-c n") #'ffs-find-speaker-notes-file)
+    map)
+  "Keymap for `ffs-minor-mode'.")
+
+(define-minor-mode ffs-minor-mode
+  "Minor mode for form feed-separated plain text presentations."
+  :group 'ffs
+  :lighter " ffs"
+  :keymap ffs-minor-mode-map
+  (setq-local
+   ffs--old-mode-line-format mode-line-format
+   ffs--old-cursor-type cursor-type
+   ffs--old-default-face-height
+   (face-attribute 'default :height))
+  (setq buffer-read-only ffs-minor-mode))
+
+(defun ffs ()
+  "Enable `ffs-minor-mode' for presenting the current buffer."
+  (interactive)
+  (ffs-minor-mode 1)
+  (setq-local ffs--slides-buffer (current-buffer)))
+
+(provide 'ffs)
+;;; ffs.el ends here
diff --git a/.emacs.d/lisp/ffs/ffsanim.el b/.emacs.d/lisp/ffs/ffsanim.el
new file mode 100644 (file)
index 0000000..cbf2969
--- /dev/null
@@ -0,0 +1,267 @@
+;;; ffsanim.el --- Form Feed Slides animate -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2022  Amin Bandali <bandali@gnu.org>
+
+;; Author: Amin Bandali <bandali@gnu.org>
+;; Version: 0.1.5
+;; Keywords: outlines, tools
+
+;; 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 <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; A simple mode for doing simple plain text presentations where the
+;; slides are separated using the form feed character (\f).  Uses
+;; animate.el to animate each slide.
+
+;; Configuration: TODO
+
+;; Usage:
+
+;; (add-to-list 'load-path (b/lisp "ffs"))
+;; (run-with-idle-timer 0.5 nil #'require 'ffsanim)
+;; (with-eval-after-load 'ffsanim
+;;   (defvar b/original-default-height)
+;;   (defvar b/ffsanim-default-height 300)
+;;   (global-set-key
+;;    (kbd "C-c f s")
+;;    (lambda ()
+;;      (interactive)
+;;      (setq
+;;       b/original-default-height (face-attribute 'default :height))
+;;      (set-face-attribute
+;;       'default nil :height b/ffsanim-default-height)
+;;      (message " ")
+;;      (ffsanim)))
+;;   (define-key
+;;    ffsanim-mode-map (kbd "q")
+;;    (lambda ()
+;;      (interactive)
+;;      (quit-window)
+;;      (set-face-attribute
+;;       'default nil :height b/original-default-height)
+;;      (message " "))))
+
+;;; Code:
+
+(require 'animate)
+
+(defgroup ffsanim nil
+  "Major mode for form feed-separated plain text presentations."
+  :version "29.1"
+  :prefix "ffsanim-")
+
+(defcustom ffsanim-buffer-name "*ffsanim*"
+  "The name of the ffsanim presentation buffer."
+  :group 'ffsanim
+  :type 'string)
+
+(defcustom ffsanim-edit-buffer-name "*ffsanim-edit*"
+  "The name of the ffsanim-edit buffer used when editing a slide."
+  :group 'ffsanim
+  :type 'string)
+
+(defvar ffsanim--source-buffer-name ""
+  "The name of the form feed-separated \"source\" buffer for a
+presentation.")
+
+(defun ffsanim--buffer ()
+  "Get the ffsanim presentation buffer."
+  (get-buffer-create ffsanim-buffer-name))
+
+(defmacro ffsanim-define-move-to-slide (name &optional doc &rest body)
+  "Define a function for moving to a slide.
+Symbol NAME is the name describing the movement.
+DOC is the documentation string to use for the function."
+  (declare (debug (&define name [&optional stringp] def-body))
+           (doc-string 2) (indent defun))
+  (when (and doc (not (stringp doc)))
+    ;; `doc' is the first element of `body', not an actual docstring
+    (push doc body)
+    (setq doc nil))
+  (let* ((sn (symbol-name name))
+         (fname (intern (format "ffsanim-%s-slide" (downcase sn)))))
+    `(defun ,fname ()
+       ,doc
+       (interactive)
+       (let ((s (progn
+                  (pop-to-buffer-same-window
+                   (get-buffer ffsanim--source-buffer-name))
+                  ,@body
+                  (narrow-to-page)
+                  (prog1 (buffer-string)
+                    (widen)
+                    (pop-to-buffer-same-window (ffsanim--buffer)))))
+             (animation-buffer-name (buffer-name (ffsanim--buffer)))
+             (inhibit-read-only t))
+         (animate-sequence (split-string s "\n") 0)))))
+
+(defun ffsanim-edit-slide (&optional add-before-or-after)
+  "Pop to a new buffer to edit a slide.
+If ADD-BEFORE-OR-AFTER is nil or not given, we are editing an
+existing slide.  Otherwise, if it is `add-before' then the new
+slide will be added before the current slide, and if it is
+`add-after' then the new slide will be added after the current
+slide.  The logic for handling this is in `ffsanim-edit-done'."
+  (interactive)
+  (let* (m
+         (s (with-current-buffer (get-buffer ffsanim--source-buffer-name)
+              (setq m major-mode)
+              (if add-before-or-after   ; if we are adding a new slide
+                  "\n"                  ; start with just a newline
+                (narrow-to-page)
+                (prog1 (buffer-string)
+                  (widen))))))
+    (pop-to-buffer-same-window
+     (get-buffer-create ffsanim-edit-buffer-name))
+    (funcall m)
+    (ffsanim-edit-mode 1)
+    (insert s)
+    (goto-char (point-min))
+    (setq-local ffsanim--new-location add-before-or-after)
+    (message
+     (substitute-command-keys "Edit, then use `\\[ffsanim-edit-done]' \
+to apply your changes or `\\[ffsanim-edit-discard]' to discard them."))))
+
+(defun ffsanim-edit-discard ()
+  "Discard current ffsanim-edit buffer and return to the presentation."
+  (interactive)
+  (let ((buf (current-buffer)))
+    (quit-windows-on buf)
+    (kill-buffer buf))
+  (pop-to-buffer-same-window (ffsanim--buffer)))
+
+(defun ffsanim-edit-done ()
+  "Apply the ffsanim-edit changes and return to the presentation."
+  (interactive)
+  (let* (f
+         (str (buffer-string))
+         (s (if (string-suffix-p "\n" str)
+                str
+              (concat str "\n")))
+         (l ffsanim--new-location))
+    (with-current-buffer (get-buffer ffsanim--source-buffer-name)
+      (save-excursion
+        (cond
+         ((eq l 'add-before)
+          (backward-page)
+          (insert (format "\n%s\f" s))
+          (setq f #'ffsanim-previous-slide))
+         ((eq l 'add-after)
+          (forward-page)
+          (insert (format "\n%s\f" s))
+          (setq f #'ffsanim-next-slide))
+         ((null l)
+          (narrow-to-page)
+          (delete-region (point-min) (point-max))
+          (insert s)
+          (widen)
+          (setq f #'ffsanim-current-slide)))))
+    (ffsanim-edit-discard)
+    (funcall f)))
+
+(defun ffsanim-new-slide-before ()
+  "Add a new slide before the current slide."
+  (interactive)
+  (ffsanim-edit-slide 'add-before))
+
+(defun ffsanim-new-slide-after ()
+  "Add a new slide after the current slide."
+  (interactive)
+  (ffsanim-edit-slide 'add-after))
+
+(defvar ffsanim--old-mode-line-format nil
+  "The value of `mode-line-format' in the ffsanim presentation buffer
+before the last call to `ffsanim--toggle-mode-line'.")
+
+(defun ffsanim--toggle-mode-line ()
+  "Toggle the display of the mode-line in the current buffer."
+  (interactive)
+  (if mode-line-format
+      (setq-local ffsanim--old-mode-line-format mode-line-format
+                  mode-line-format nil)
+    (setq-local mode-line-format ffsanim--old-mode-line-format
+                ffsanim--old-mode-line-format nil))
+  (redraw-display))
+
+(ffsanim-define-move-to-slide previous
+  "Go to the previous slide."
+  (backward-page)
+  (backward-page))
+
+(ffsanim-define-move-to-slide next
+  "Go to the next slide."
+  (forward-page))
+
+(ffsanim-define-move-to-slide current
+  "Reload and renimate the current slide."
+  nil)
+
+(ffsanim-define-move-to-slide first
+  "Go to the first slide."
+  (goto-char (point-min)))
+
+(ffsanim-define-move-to-slide last
+  "Go to the last slide."
+  (goto-char (point-max)))
+
+(define-derived-mode ffsanim-mode special-mode "ffsanim"
+  "Major mode for form feed-separated plain text presentations."
+  :group 'ffsanim
+  :interative nil
+  (setq-local animate-total-added-delay 0.3)
+  (show-paren-local-mode -1)
+  (display-battery-mode -1)
+  (ffsanim--toggle-mode-line)
+  (ffsanim-current-slide))
+
+(defvar ffsanim-edit-mode-map
+  (let ((map (make-sparse-keymap)))
+    (define-key map (kbd "C-c C-k") #'ffsanim-edit-discard)
+    (define-key map (kbd "C-c C-c") #'ffsanim-edit-done)
+    map)
+  "Keymap for `ffsanim-edit-mode'.")
+
+(define-minor-mode ffsanim-edit-mode
+  "Minor mode for editing a single ffsanim slide.
+When done editing the slide, run \\[ffsanim-edit-done] to apply your
+changes, or \\[ffsanim-edit-discard] to discard them."
+  :group 'ffsanim
+  :lighter " ffsanim-edit"
+  :keymap ffsanim-edit-mode-map
+  (defvar-local ffsanim--new-location nil
+    "The location where the new slide should be inserted.
+See the docstring for `ffsanim-edit-slide' for more details."))
+
+(define-key ffsanim-mode-map (kbd "p") #'ffsanim-previous-slide)
+(define-key ffsanim-mode-map (kbd "n") #'ffsanim-next-slide)
+(define-key ffsanim-mode-map (kbd "DEL") #'ffsanim-previous-slide)
+(define-key ffsanim-mode-map (kbd "SPC") #'ffsanim-next-slide)
+(define-key ffsanim-mode-map (kbd "g") #'ffsanim-current-slide)
+(define-key ffsanim-mode-map (kbd "<") #'ffsanim-first-slide)
+(define-key ffsanim-mode-map (kbd ">") #'ffsanim-last-slide)
+(define-key ffsanim-mode-map (kbd "e") #'ffsanim-edit-slide)
+(define-key ffsanim-mode-map (kbd "O") #'ffsanim-new-slide-before)
+(define-key ffsanim-mode-map (kbd "o") #'ffsanim-new-slide-after)
+(define-key ffsanim-mode-map (kbd "m") #'ffsanim--toggle-mode-line)
+
+(defun ffsanim ()
+  "Start an ffsanim presentation with current buffer as source."
+  (interactive)
+  (setq ffsanim--source-buffer-name (buffer-name))
+  (pop-to-buffer-same-window (ffsanim--buffer))
+  (ffsanim-mode))
+
+(provide 'ffsanim)
+;;; ffsanim.el ends here