1 ;;; ffs.el --- Form Feed Slides mode -*- lexical-binding: t; -*-
3 ;; Copyright (C) 2022 Amin Bandali <bandali@gnu.org>
5 ;; Author: Amin Bandali <bandali@gnu.org>
7 ;; Keywords: outlines, tools
9 ;; This program is free software; you can redistribute it and/or modify
10 ;; it under the terms of the GNU General Public License as published by
11 ;; the Free Software Foundation, either version 3 of the License, or
12 ;; (at your option) any later version.
14 ;; This program is distributed in the hope that it will be useful,
15 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
16 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 ;; GNU General Public License for more details.
19 ;; You should have received a copy of the GNU General Public License
20 ;; along with this program. If not, see <https://www.gnu.org/licenses/>.
24 ;; A simple mode for doing simple plain text presentations where the
25 ;; slides are separated using the form feed character (\f).
27 ;; Configuration: TODO
31 ;; Put this file, ffs.el, in a directory in your `load-path', then add
32 ;; something like the following to your init file:
35 ;; (global-set-key (kbd "C-c f s") #'ffs)
37 ;; Then, open a text file/buffer that you would like you to use as the
38 ;; source of your presentation and type `M-x ffs RET' or a keyboard
39 ;; shortcut you defined (like the above example) to start ffs, at
40 ;; which point you should be able to see "ffs" appear as one of the
41 ;; currently enabled minor modes in your mode-line. Once ffs is
42 ;; enabled, you can invoke its various commands. To see a list of
43 ;; available commands, you can either type `M-x ffs- TAB' (to get a
44 ;; completion of commands starting with the "ffs-" prefix), or see the
45 ;; definition of `ffs-minor-mode-map' near the end of this file.
50 "Minor mode for form feed-separated plain text presentations."
54 (defcustom ffs-default-face-height
370
55 "The value of the `height' property for the `default' face to use
56 during the ffs presentation."
58 :type
'(choice (const nil
)
59 (integer :value
300)))
61 (defcustom ffs-edit-buffer-name
"*ffs-edit*"
62 "The name of the ffs-edit buffer used when editing a slide."
66 (defvar ffs--slides-buffer nil
67 "The main ffs presentation slides buffer.
68 When the user enables ffs in a buffer using `\\[ffs]', we store a
69 reference to that buffer in this variable.
71 As a special case, in a speaker notes buffer selected by the user
72 using `\\[ffs-find-speaker-notes-file]' from the main ffs slides
73 buffer, this variable will point to the main ffs slides buffer
74 rather than the speaker notes buffer.")
76 (defvar ffs--notes-buffer nil
77 "The ffs speaker notes buffer (only if selected).
78 When the user chooses (and opens) a speaker notes file using
79 `\\[ffs-find-speaker-notes-file]', a reference to the file's
80 corresponding buffer is stored in this variable, local to the
81 main ffs presentation slides buffer (`ffs--slides-buffer').")
83 (defvar ffs--old-mode-line-format nil
84 "The old value of `mode-line-format' before enabling
85 `ffs--no-mode-line-minor-mode'.")
87 (defvar ffs--old-cursor-type nil
88 "The old value of `cursor-type' before enabling
89 `ffs--no-cursor-minor-mode'.")
91 (defvar ffs--old-default-face-height nil
92 "The old value of the `default' face's `height' property before
93 starting the ffs presentation.")
95 (define-minor-mode ffs--no-mode-line-minor-mode
96 "Minor mode for hiding the mode-line."
98 (if ffs--no-mode-line-minor-mode
100 (unless ffs--old-mode-line-format
101 (setq-local ffs--old-mode-line-format mode-line-format
))
102 (setq-local mode-line-format nil
))
103 (setq-local mode-line-format ffs--old-mode-line-format
)
104 (when ffs--old-mode-line-format
105 ffs--old-mode-line-format nil
))
108 (define-minor-mode ffs--no-cursor-minor-mode
109 "Minor mode for hiding the cursor."
111 (if ffs--no-cursor-minor-mode
113 (unless ffs--old-cursor-type
114 (setq-local ffs--old-cursor-type cursor-type
))
115 (setq-local cursor-type nil
))
116 (setq-local cursor-type ffs--old-cursor-type
)
117 (when ffs--old-cursor-type
118 ffs--old-cursor-type nil
)))
120 (defun ffs--toggle-dark-mode ()
121 "Swap the frame background and foreground colours."
123 (let ((bg (frame-parameter nil
'background-color
))
124 (fg (frame-parameter nil
'foreground-color
)))
125 (set-background-color fg
)
126 (set-foreground-color bg
)))
128 (defun ffs--goto-previous (buffer)
129 "Go to the previous slide in the given BUFFER."
131 (with-current-buffer buffer
132 (let ((n (buffer-narrowed-p)))
134 (goto-char (point-min))
138 (when n
(narrow-to-page)))))
140 (defun ffs-goto-previous ()
141 "Go to the previous slide in the main ffs presentation and the
142 speaker notes buffer (if any)."
144 (ffs--goto-previous ffs--slides-buffer
)
145 (when ffs--notes-buffer
146 (ffs--goto-previous ffs--notes-buffer
)
149 (defun ffs--goto-next (buffer)
150 "Go to the next slide in the given BUFFER."
152 (with-current-buffer buffer
153 (let ((n (buffer-narrowed-p))
154 (e (= (- (point-max) (point-min)) 0)))
156 (goto-char (point-min))
158 (unless e
(forward-page))
159 (when n
(narrow-to-page)))))
161 (defun ffs-goto-next ()
162 "Go to the next slide in the main ffs presentation and the
163 speaker notes buffer (if any)."
165 (ffs--goto-next ffs--slides-buffer
)
166 (when ffs--notes-buffer
167 (ffs--goto-next ffs--notes-buffer
)
170 (defun ffs--goto-first (buffer)
171 "Go to the first slide in the given BUFFER."
173 (with-current-buffer buffer
174 (let ((n (buffer-narrowed-p)))
176 (goto-char (point-min))
177 (when n
(narrow-to-page)))))
179 (defun ffs-goto-first ()
180 "Go to the first slide in the main ffs presentation and the
181 speaker notes buffer (if any)."
183 (ffs--goto-first ffs--slides-buffer
)
184 (when ffs--notes-buffer
185 (ffs--goto-first ffs--notes-buffer
)
188 (defun ffs--goto-last (buffer)
189 "Go to the last slide in the given BUFFER."
191 (let ((n (buffer-narrowed-p)))
193 (goto-char (point-max))
194 (when n
(narrow-to-page))))
196 (defun ffs-goto-last ()
197 "Go to the last slide in the main ffs presentation and the
198 speaker notes buffer (if any)."
200 (ffs--goto-last ffs--slides-buffer
)
201 (when ffs--notes-buffer
202 (ffs--goto-last ffs--notes-buffer
)
206 "Start the presentation."
209 (ffs--no-mode-line-minor-mode 1)
210 (ffs--no-cursor-minor-mode 1)
211 (when (integerp ffs-default-face-height
)
213 ffs--old-default-face-height
214 (face-attribute 'default
:height
))
215 (face-remap-add-relative
216 'default
:height ffs-default-face-height
))
217 (show-paren-local-mode -
1)
218 (display-battery-mode -
1)
223 "Quit the presentation."
225 (let ((n (buffer-narrowed-p))
226 (e (= (- (point-max) (point-min)) 0)))
227 (when (integerp ffs-default-face-height
)
228 (face-remap-add-relative
229 'default
:height ffs--old-default-face-height
))
230 (show-paren-local-mode 1)
231 (display-battery-mode 1)
233 (ffs--no-mode-line-minor-mode -
1)
234 (ffs--no-cursor-minor-mode -
1)
237 (goto-char (point-min))
240 (when e
(forward-char -
1))))
242 (defun ffs-edit (&optional add-above-or-below
)
243 "Pop to a new buffer to edit a slide.
244 If ADD-ABOVE-OR-BELOW is nil or not given, we are editing an
245 existing slide. Otherwise, if it is `add-above' then the new
246 slide will be added above/before the current slide, and if it is
247 `add-below' then the new slide will be added below/after the
248 current slide. The logic is implemented in `ffs-edit-done'."
250 (let* ((b (current-buffer))
252 (n (buffer-narrowed-p))
253 (s (if add-above-or-below
; if we are adding a new slide
254 "\n" ; start with just a newline
255 (unless n
(narrow-to-page))
256 (prog1 (buffer-string)
257 (unless n
(widen))))))
258 (pop-to-buffer-same-window
259 (get-buffer-create ffs-edit-buffer-name
))
261 (ffs-edit-minor-mode 1)
263 (goto-char (point-min))
264 (set-buffer-modified-p nil
)
266 ffs--edit-source-buffer b
267 ffs--new-location add-above-or-below
)
269 (substitute-command-keys "Edit, then use `\\[ffs-edit-done]' \
270 to apply your changes or `\\[ffs-edit-discard]' to discard them."))))
272 (defun ffs-new-above ()
273 "Add a new slide above/before the current slide."
275 (ffs-edit 'add-above
))
277 (defun ffs-new-below ()
278 "Add a new slide below/after the current slide."
280 (ffs-edit 'add-below
))
282 (defun ffs-edit-discard ()
283 "Discard current ffs-edit buffer and return to the presentation."
285 (let ((b (current-buffer)))
289 (defun ffs-edit-done ()
290 "Apply the ffs-edit changes and return to the presentation."
293 (str (buffer-string))
294 (s (if (string-suffix-p "\n" str
)
297 (l ffs--new-location
))
298 (with-current-buffer ffs--edit-source-buffer
299 (let ((inhibit-read-only t
))
304 (insert (format "\n%s\f" s
))
305 (setq f
#'ffs-previous-slide
))
308 (insert (format "\n%s\f" s
))
309 (setq f
#'ffs-next-slide
))
312 (delete-region (point-min) (point-max))
319 (defun ffs--undo (&optional arg
)
320 "Like `undo', but it works even when the buffer is read-only."
322 (let ((inhibit-read-only t
))
325 (defun ffs-find-speaker-notes-file (file)
326 "Prompt user for a speaker notes file, open it in a new frame."
327 (interactive "Fspeakers notes buffer: ")
328 (let ((b (current-buffer)))
330 (find-file-other-frame file
)
334 ffs--notes-buffer
(current-buffer)))
335 (setq-local ffs--notes-buffer
(get-file-buffer file
))))
337 (defun ffs-export-slides-to-pdf ()
339 (with-current-buffer ffs--slides-buffer
342 (fringe fringe-mode
))
345 (let ((fn (format "%s-%03d.pdf"
346 (file-name-sans-extension (buffer-name))
348 (data (x-export-frames nil
'pdf
)))
353 (fringe-mode fringe
))))
355 (defvar ffs-edit-minor-mode-map
356 (let ((map (make-sparse-keymap)))
357 (define-key map
(kbd "C-c C-k") #'ffs-edit-discard
)
358 (define-key map
(kbd "C-c C-c") #'ffs-edit-done
)
360 "Keymap for `ffs-edit-minor-mode'.")
362 (define-minor-mode ffs-edit-minor-mode
363 "Minor mode for editing a single ffs slide.
364 When done editing the slide, run \\[ffs-edit-done] to apply your
365 changes, or \\[ffs-edit-discard] to discard them."
368 :keymap ffs-edit-minor-mode-map
369 (defvar-local ffs--edit-source-buffer nil
370 "The ffs presentation buffer of the slide being edited.")
371 (defvar-local ffs--new-location nil
372 "The location where the new slide should be inserted.
373 See the docstring for `ffs-edit' for more details."))
375 (defvar ffs-minor-mode-map
376 (let ((map (make-sparse-keymap)))
377 (define-key map
(kbd "p") #'ffs-goto-previous
)
378 (define-key map
(kbd "n") #'ffs-goto-next
)
379 (define-key map
(kbd "DEL") #'ffs-goto-previous
)
380 (define-key map
(kbd "SPC") #'ffs-goto-next
)
381 (define-key map
(kbd "[") #'ffs-goto-previous
)
382 (define-key map
(kbd "]") #'ffs-goto-next
)
383 (define-key map
(kbd "<") #'ffs-goto-first
)
384 (define-key map
(kbd ">") #'ffs-goto-last
)
385 (define-key map
(kbd "s") #'ffs-start
)
386 (define-key map
(kbd "q") #'ffs-quit
)
387 (define-key map
(kbd "e") #'ffs-edit
)
388 (define-key map
(kbd "O") #'ffs-new-above
)
389 (define-key map
(kbd "o") #'ffs-new-below
)
390 (define-key map
(kbd "m") #'ffs--no-mode-line-minor-mode
)
391 (define-key map
(kbd "c") #'ffs--no-cursor-minor-mode
)
392 (define-key map
(kbd "d") #'ffs--toggle-dark-mode
)
393 (define-key map
(kbd "N") #'narrow-to-page
)
394 (define-key map
(kbd "W") #'widen
)
395 (define-key map
[remap undo
] #'ffs--undo
)
396 (define-key map
(kbd "C-c n") #'ffs-find-speaker-notes-file
)
398 "Keymap for `ffs-minor-mode'.")
400 (define-minor-mode ffs-minor-mode
401 "Minor mode for form feed-separated plain text presentations."
404 :keymap ffs-minor-mode-map
406 ffs--old-mode-line-format mode-line-format
407 ffs--old-cursor-type cursor-type
408 ffs--old-default-face-height
409 (face-attribute 'default
:height
))
410 (setq buffer-read-only ffs-minor-mode
))
413 "Enable `ffs-minor-mode' for presenting the current buffer."
416 (setq-local ffs--slides-buffer
(current-buffer)))