From 22758e8c9214f2086fe0c0a424c993f0a52a5780 Mon Sep 17 00:00:00 2001 From: Amin Bandali Date: Thu, 19 May 2022 21:57:45 -0400 Subject: Add ffs (form feed slides) mode for GNU Emacs This is what I used for preparing and presenting my LibrePlanet 2022 talk, 'The Net beyond the web' back in March. :) --- .emacs.d/lisp/ffs/ffs.el | 419 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 419 insertions(+) create mode 100644 .emacs.d/lisp/ffs/ffs.el (limited to '.emacs.d/lisp/ffs/ffs.el') diff --git a/.emacs.d/lisp/ffs/ffs.el b/.emacs.d/lisp/ffs/ffs.el new file mode 100644 index 0000000..99f2097 --- /dev/null +++ b/.emacs.d/lisp/ffs/ffs.el @@ -0,0 +1,419 @@ +;;; ffs.el --- Form Feed Slides mode -*- lexical-binding: t; -*- + +;; Copyright (C) 2022 Amin Bandali + +;; Author: Amin Bandali +;; 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 . + +;;; Commentary: + +;; A simple mode for doing simple plain text presentations where the +;; slides are separated using the form feed character ( ). + +;; 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 " s)) + (setq f #'ffs-previous-slide)) + ((eq l 'add-below) + (forward-page) + (insert (format "\n%s " 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 -- cgit v1.2.3-60-g2f50