-
-
Notifications
You must be signed in to change notification settings - Fork 4
/
dslide.el
3875 lines (3432 loc) · 159 KB
/
dslide.el
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
;;; dslide.el --- Domain Specific sLIDEs. Programmable Presentation -*- lexical-binding: t; -*-
;;
;; Copyright (C) 2011-2023 Takaaki ISHIKAWA
;; Copyright (C) 2024 Positron
;;
;; Author: Positron <contact@positron.solutions>
;; Version: 0.6.2
;; Package-Requires: ((emacs "29.2"))
;; Maintainer: Positron <contact@positron.solutions>
;; URL: https://github.com/positron-solutions/dslide
;; Keywords: convenience, org-mode, presentation, narrowing
;;
;; Committers: Takaaki ISHIKAWA <takaxp at ieee dot org>
;; Yuuki ARISAWA (@uk-ar)
;; Eric S Fraga
;; Eike Kettner
;; Stefano BENNATI
;; Matus Goljer
;; Boruch Baum
;;
;;; Copying:
;; 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 GNU Emacs; see the file COPYING. If not, write to the Free Software
;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
;; USA.
;;; Commentary:
;; DSL IDE creates presentations out of org mode documents. Every single step
;; in a presentation can be individually configured, customized, or
;; programmed. Org headings and elements are configured with extensible
;; actions. Custom steps can be scripted with babel blocks. Keyboard macros
;; can play back real command sequences. Frequent customizations can be made
;; into custom actions. DSL IDE achieves a good result with no preparation
;; but can achieve anything Emacs can display if you need it to.
;;
;; To try it out, install this package and load the demo.org found in the test
;; directory of the repository. `dslide-deck-start' will begin the
;; presentation and the first slides tell you how to progress, like a
;; tutorial. The README for the repository is generated from the manual and
;; explains conceptually the meaning of the examples in the demo.
;;
;; Requirement:
;; org-mode 9.6.29 or higher version
;; The latest version of the org-mode is recommended.
;; (see https://orgmode.org/)
;;
;; Configuring:
;; M-x customize-group RET dslide RET
;;
;; Customizing & Extending:
;;
;; For high level overview of the key concepts present in this Elisp file, see
;; the Hacking section of the dslide manual, available in completions for
;; `info-display-manual'. The package code has key areas documented to expand
;; on ideas in the manual, using docstrings and more technical commentary
;; closer to the source.
;;
;; This package began as a fork and became a complete re-write of
;; org-tree-slide by Takaaki ISHIKAWA. Thanks to everyone who worked on
;; org-tree-slide over the years. The implementation ideas and features of
;; org-tree-slide were a great inspiration for this package. Long live
;; 🖊️🍍🍎🖊️.
;;; Code:
(require 'image-mode)
(require 'eieio)
(require 'org-element)
(require 'org-fold)
(require 'face-remap)
(eval-when-compile (require 'cl-lib))
(defgroup dslide nil
"User variables for `dslide'."
:group 'outlines)
(defcustom dslide-base-follows-slide t
"Non-nil moves the base buffer point to the current slide.
This happens whether the buffer is visible or not."
:type 'boolean)
(defcustom dslide-start-from 'point
"When starting, begin at `point' `first' slide.
Any other value is equivalent to `first'.
If the contents are shown first, the point will be on the configured
slide.
This only has effect when starting the mode or commands that implicitly
start the mode.
- `first': Always begin the slideshow from the very first slide.
- `point': the slideshow always begins at the slide under point.
If you just want to navigate slides with the point, you should use the
contents mode by calling `dslide-deck-start' in a presentation that is
already started."
:type '(choice (const :tag "First slide" first)
(const :tag "Slide at point" point)))
(defcustom dslide-start-function #'dslide-display-slides
"When starting the mode, this is the default starting function.
It should usually call `dslide-display-slides' or
`dslide-display-contents'. You can build commands that use `let'
binding to temporarily set this variable in order to start with a
custom starting function."
:type 'function)
(defcustom dslide-header t
"Display header in contents buffer.
When this is disabled, the keywords for title etc will remain visible,
albeit scrolled away because of how `org-overview' works."
:type 'boolean)
(defcustom dslide-header-fun nil
"Custom function to override heading generation.
The function you define should accept two arguments:
- CLEANUP: meaning to delete any state that was created for an
existing header.
- optional BREADCRUMBS: indicating if creating breadcrumbs is
appropriate or not, such as when displaying the contents view.
🚧 This option is experimental and the signature is subject to
change.
When nil, the header is generated by the default
`dslide-make-header', which respects many customize options.
However, you may find it faster to completely replace this
function to get exactly what you want."
:type 'function)
(defcustom dslide-header-author t
"Show the email in the header.
If there is a #+author: keyword, it will be used."
:type 'boolean)
(defcustom dslide-header-email t
"Show the email in the header.
If there is a #+email: keyword, it will be used."
:type 'boolean)
(defcustom dslide-header-date t
"Show the date in the header.
If there is a #+date: keyword, it will be used.
The current time will be used as a fallback."
:type 'boolean)
(defcustom dslide-margin-title-above 0.5
"Margin between header title and the top of the window.
Can be a float or integer."
:type 'number)
(defcustom dslide-margin-title-below 0.5
"Margin between title and other header info.
Can be a float or integer."
:type 'number)
(defcustom dslide-margin-content 1.5
"Margin between the slide header and its content.
Can be a float or integer."
:type 'number)
(defcustom dslide-slide-in-effect t
"Using a visual effect of slide-in for displaying trees."
:type 'boolean)
(defcustom dslide-slide-in-blank-lines 15
"Line height of the slide-in effect."
:type 'number)
(defcustom dslide-feedback-messages
'(:start "Start! ▶"
:forward "Forward →"
:backward "← Backward"
:contents "Contents ☰"
:stop "Stop ■"
:after-last-slide "No more slides!")
"Feedback messages for slide controls.
Turn off by setting to nil. Plist keys and where they are used:
- :start `dslide-deck-start'
- :forward `dslide-deck-forward'
- :backward `dslide-deck-backward'
- :contents `dslide-display-contents'
- :stop `dslide-deck-stop'
- :after-last-slide: see `after-last-slide-hook'"
:type 'plist
:options '((:start string)
(:forward string)
(:backward string)
(:contents string)
(:stop string)
(:after-last-slide string)))
(defcustom dslide-breadcrumb-face '(:inherit org-level-8)
"Face added to the list of faces for breadcrumbs.
This can be a face name symbol or an anonymous face spec. It will be
added to the face list, meaning it the original face's properties remain
unless shadowed."
:type 'face)
(defcustom dslide-breadcrumb-separator " 🢒 "
"Delimiter for breadcrumbs or nil to turn off breadcrumbs."
:type '(choice (const :tag "Don't display breadcrumbs" nil)
(string :tag "Delimiter")))
(defcustom dslide-breadcrumb-separator-style 'append
"Where breadcrumb separators will be used.
This helps distinguish the breadcrumbs from the slide headline. Use
append to have a terminal breadcrumb or separate to only put them
between breadcrumbs."
:type '(choice (const :tag "After each breadcrumb" append)
(const :tag "Only between breadcrumbs" separate)))
(define-obsolete-variable-alias
'dslide-breadcrumbs-hide-todo-state 'dslide-breadcrumb-hide-todo-state
"0.5.5")
(defcustom dslide-breadcrumb-hide-todo-state t
"If non-nil, hide TODO states in the breadcrumbs."
:type 'boolean)
(defcustom dslide-hide-todo t
"If non-nil, hide TODO states in headings."
:type 'boolean)
(defcustom dslide-hide-tags t
"If non-nil, hide tags in headings."
:type 'boolean)
(defcustom dslide-hide-markup-types '(comment
comment-block
drawer
export-block
property-drawer
keyword)
"Default types to be hidden by `dslide-action-hide-markup'.
Can be any element in `org-element-all-elements'."
:type '(repeat symbol))
(defcustom dslide-animation-duration 1.0
"How long slide in takes."
:type 'number)
(defcustom dslide-animation-frame-duration (/ 1.0 60.0)
"Length between updates.
Increase if your so-called machine has trouble drawing."
:type 'number)
(defcustom dslide-start-hook '(dslide-cursor-hide)
"Runs when deck is started with `dslide-deck-start'.
Runs after the slide buffer is created but before first slide runs
`dslide-begin'. Buffer is widened and fully visible.
It is intended only to run when `dslide-mode' is first enabled, so your
hook functions do not need to be idempotent.
🚧 This hook is not experimental. However, the lifecycle management of
a deck is somewhat experimental. Please report issues."
:type 'hook)
(defcustom dslide-present-hook '(dslide-cursor-hide)
"Runs when a deck is started with `dslide-deck-present'.
Runs after the slide buffer is created but before first slide calls
`dslide-begin'. Buffer is widened and fully visible.
Use this hook to customize your presentation frame or the slide buffer
it displays. It is intended only to run on the first call, which sets
up the frame, so your hook functions do not need to be idempotent.
🚧 This hook was recently added. Its purpose is clear. The lifecycle
management of a deck is somewhat experimental. Please report issues."
:type 'hook)
(defcustom dslide-develop-hook '(dslide-cursor-hide)
"Runs when a presentation is started with `dslide-deck-develop'.
Runs after the slide buffer is created but before first slide calls
`dslide-begin'. Buffer is widened and fully visible.
Use this hook to set up the slide buffer and window it is displayed in.
It is intended only to run on the first call, which creates the window,
so your hook functions do not need to be idempotent.
🚧 This hook was recently added. Its purpose is clear. The lifecycle
management of a deck is somewhat experimental. Please report issues."
:type 'hook)
(defcustom dslide-stop-hook nil
"Runs in the base buffer after stopping."
:type 'hook)
(defcustom dslide-narrow-hook nil
"Runs whenever the slide buffer restriction is updated.
Use this hook for behaviors that affect the displayed region. Slides
and sequences that do not display themselves or only affect display in
another buffer will not trigger this hook."
:type 'hook)
(defcustom dslide-contents-hook nil
"Runs last after switching to contents."
:type 'hook)
(defcustom dslide-after-last-slide-hook '()
"Runs when forward is called but there is no next slide.
This can either provide feedback or quit immediately etc. Consider
using `dslide-push-step' and writing a callback that only reacts to the
`forward' state. This callback will then only run if the user
immediately calls `dslide-deck-forward' again. `dslide-deck-stop' is
another good choice."
:type 'hook)
(defcustom dslide-present-frame-parameters nil
"Frame parameters used when creating a frame with `dslide-deck-present'.
These are combined with the `default-frame-alist' parameters See the
info node `(elisp)Creating Frames'.
Use `dslide-present-hook' to customize the created frame
further.
It's worth mentioning that you should know about
`frame-inhibit-implied-resize'. This can help with the frame being
resized by resizing text or changing other settings. ."
:type 'alist)
(defcustom dslide-default-slide-action #'dslide-slide-action-child
"Action class with lifecycle around the section actions.
When stepping forward or backward, it is called before any section
action. It's normal purpose is to update the buffer restriction before
section actions are run.
You can configure this per-heading by setting the SLIDE_ACTION keyword.
You can configure it for the document default by adding an SLIDE_ACTION
keyword."
:type 'function)
;; TODO test the use of plist args
(defcustom dslide-default-actions '(dslide-action-hide-markup
dslide-action-propertize
dslide-action-kmacro
dslide-action-babel
dslide-action-image)
"Actions that run within the section display action lifecycle.
It's value is a list of symbol `dslide-action' sub-classes or (CLASS
. ARGS) forms where ARGS is a plist. Each subclass will be instantiated
into an action object. See the symbol `dslide-action' class and its
methods to learn about writing custom actions.
Many section actions are no-op whenever the content doesn't contain any
elements they act on. You can add classes to this list in order to have
default behaviors for some org elements.
⛔ Planned deprecation. Actions will be dispatched on-demand in the
future rather than turned on for each slide.
You can configure actions per-heading by setting the DSLIDE_ACTIONS
property. You can configure it for the document default by adding an
DSLIDE_ACTIONS keyword."
:type '(repeat function))
(defcustom dslide-default-class 'dslide-slide
"A class to more deeply modify slide behavior.
Value should be a custom class extending `dslide'. You can override
methods if the built-in implementation is insufficient. Consider
upstreaming changes.
You can configure this per heading by setting the DSLIDE_CLASS property.
You can configure it for the document default by adding an DSLIDE_CLASS
keyword."
:type 'symbol)
(defcustom dslide-default-deck-class 'dslide-deck
"A class to more deeply modify overall deck behavior.
Value should be a custom class extending symbol `dslide-deck'. Use this
to modify the root-level behaviors, including switching to children and
finding siblings. You can configure this for the document by adding the
DSLIDE_ROOT_CLASS keyword.
🚧 This exists, but is likely very rarely used or tested."
:type 'symbol)
(defcustom dslide-default-filter #'dslide-built-in-filter
"A function used to call next on children.
The function used as actions should accept an org element, a `headline'
type element and return the element if it is a valid heading or return
nil if it should be skipped.
You can configure this per heading by setting the DSLIDE_FILTER keyword.
You can configure it for the document default by adding an DSLIDE_FILTER
keyword."
:type 'function)
(defcustom dslide-contents-selection-highlight t
"Show a highlight on the selected headline.
This is useful if you have some subtle cursor feature enabled for your
presentation and wouldn't otherwise know what line you are on in the
contents view. The default is also just a way more obvious display
style."
:type 'boolean)
(defcustom dslide-kmacro-transcribe-hook nil
"Hook run whenever dslide transcribes a keyboard macro.
Maybe add a highlight on the recorded macro to make it more obvious when
a new one was recorded. I don't know. It's your hook. I just work
here."
:type 'hook)
(defcustom dslide-kmacro-transcribe-prompt "Label for transcribed macro: "
"Ask for a comment to label newly transcribed macros.
Transcribed macros can have a comment prepended to make it obvious what
they do. Set this prompt to nil if you don't wish to be bothered while
recording."
:type 'string)
(defcustom dslide-kmacro-transcribe-type :keys
"Input type for transcribed kmacros."
:type '(choice (const :tag "Human readable keys" :keys)
(const :tag "Emacs char events" :events)))
(defface dslide-contents-selection-face
'((t :inherit org-level-1 :inverse-video t :extend t))
"Face for highlighting the current slide root.")
(defface dslide-highlight
'((t :inherit hl-line))
"Face used in base buffer to highlight progress.
See `dslide-base-follows-slide'.")
(defface dslide-babel-success-highlight
'((t :inherit hl-line))
"Temporarily highlight babel blocks that succeeded.")
(defface dslide-babel-error-highlight
'((t :inherit error))
"Temporarily highlight babel blocks that failed.")
(defvar dslide--debug nil
"Set to t for logging slides and actions.")
(defvar dslide--animation-timers nil)
(defvar-local dslide--animation-overlays nil)
(defvar dslide--kmacro-timer nil
"Allow cleanup and prevent macros from running concurrently.")
(defvar dslide--kmacro-transcribe-mark nil
"Marker storage for macro transcription.")
(defvar dslide--kmacro-transcribe-last nil
"Most recently transcribed keyboard macro.
Uses eq comparison in case there are two calls to
`kmacro-end-macro' for the same macro.")
;; Tell the compiler that these variables exist
(defvar dslide-mode)
(defvar dslide--deck nil
"Active deck object.
This is global. If a presentation is active, you can look at this variable to
coordinate with it.")
(defvar dslide-overlays nil
"Overlays used to hide or change contents display.
These are cleaned up when the restriction is changed or when switching
between contents and slides.")
(defvar dslide--step-overlays nil
"Overlays that only live for one step.")
(defvar dslide--header-overlay nil
"Flag to check the status of overlay for a slide header.")
;; Shouldn't need one per buffer
(defvar dslide--contents-hl-line-overlay nil
"Highlights selected heading in contents view.")
(defvar dslide--present-frame nil
"If we create a frame, we track a frame.")
(defvar dslide--develop-window-config nil
"If we create a window, we track a window.")
(defconst dslide--display-actions
'(display-buffer-same-window display-buffer-in-previous-window)
"Configure `display-buffer-alist' to override.")
;; * Classes
;; - `dslide-deck': is the first thing called into by `dslide-deck-forward' and
;; `dslide-deck-backward'. It chooses root headings to hydrate as slides and forwards
;; these commands into the slides.
;;
;; - `dslide-slide': interprets an org heading into some actions and coordinates
;; forwarding calls into actions in the correct order. Through their actions,
;; slides may forward calls into other slides.
;;
;; - `dslide-action': does most of the actual work of narrowing, hiding,
;; animating, executing babel etc. Actions with `slide-action' in their name
;; likely hydrate child slides and forward calls into them.
;; The generic functions below are the most important interfaces for all hacking
;; of this package.
;;
;; The domain model first must describe a linear sequence of steps that the user
;; traverses both forward and backward.
;;
;; There are some states that may need to be set up or torn down at the
;; boundaries of the sequence. These are handled by three methods:
;; - `dslide-begin'
;; - `dslide-end'
;; - `dslide-final'
;;
;; `dslide-end' is essentially begin for going in reverse. Usually this is
;; the same as calling begin and then stepping forward until no more progress
;; is made. However doing it this way would be unable to avoid extra work and
;; could even create headaches when implementing sequences that shouldn't use
;; reverse to un-execute the forwards steps or in cases where implementing
;; this is too complex to pay off to the user. For these reasons, the
;; implementation of `dslide-end' is left up to the user.
;;
;; `dslide-goto' essentially is just a careful use of forward. If every
;; forward step properly reports its maximum extent of progress, we can use
;; forward and begin to implement every goto. 🚧 The design is sound but this
;; is largely unimplemented and will not be worked on without demand.
;;
;; Finally, `dslide-forward' and `dslide-backward' should navigate the states
;; between begin or end and final. They just return non-nil until they are
;; done. The caller doesn't care about the implementation, and that is why
;; EIEIO is used.
;;
;; Sub-sequences can rely on the parent state to exist for their entire
;; lifetime. The parent sequence will not call its own `dslide-final' until
;; after it has called the sub-sequence's `dslide-final'.
;;
;; Sub-sequences currently don't have any first-class extensible support for
;; entering or exiting the sub-sequence. Such cooperation is present in
;; limited amounts to limit coupling the parent and child sequences.
;;
;; A lazy implementer can forego methods by delegating them to simpler
;; idempotent methods, such as using an idempotent begin for backward. With a
;; maximum of six methods and a minimum of two, just begin and forward, you
;; have enough behavior to properly fit the user interface.
(cl-defgeneric dslide-begin (obj)
"Set up the initial state of OBJ when going forward.
The sequence is being entered from its beginning.
Return values are ignored. `dslide-begin' always counts as a step
because it's a result of a nil return from `dslide-deck-forward'.
This method should work together with `dslide-end' and `dslide-final' to
ensure consistently valid state for `dslide-deck-forward' and
`dslide-deck-backward'.")
(cl-defgeneric dslide-end (obj)
"Set up the initial state of OBJ when going backward.
The sequence is being entered from the end.
Return values are ignored. `dslide-end' always counts as a step because
it's a result of a nil return from `dslide-deck-backward'.
The first job of this method is to perform setup, possibly by just
calling begin since they likely have similar side-effects.
Second, this method should reach the state that is equivalent to if the
user called forward until no more progress could be made.
The default implementation calls `dslide-begin' and then calls
`dslide-forward' until no more progress can be made. If this is
inappropriate, it should be overridden.
In cases where you don't need a real backward implementation or
progressing backwards would have no sensible behavior, you can delegate
this to `dslide-begin' and possibly delegate `dslide-deck-backward' to
`dslide-deck-forward', resulting in a sequence that always starts at the
beginning and always proceeds to the end. For a single step sequence
that has identical effect in both directions, this is appropriate.
This method should work together with `dslide-end' and `dslide-final' to
ensure consistently valid state for `dslide-deck-forward' and
`dslide-deck-backward'")
(cl-defgeneric dslide-final (obj)
"Clean up any remaining state of OBJ.
Implement this method to clean up any state that would interfere with
the sequence succeeding when run again. If your sequence implements
real backward behavior,
All side-effects and states created by steps in the sequence or the
`dslide-begin' and `dslide-end' methods must be cleaned up or otherwise
managed or else `dslide-backward' and other sequences of running a
presentation will be brittle and likely fail when re-run.")
(cl-defgeneric dslide-forward (obj)
"Advance OBJ forward by one step.
The return value has meaning to the deck:
- t: progress was made
- a point: progress was made up to a specific buffer location
- an org element: progress was made up to the :start property of
the element
- non-nil: ⚠ progress was made, but this value will warn because
the callee evedently returned something haphazardly
- nil: no progress could be made.
For sequences that don't make progress in a buffer, returning t is fine.
Returning a point of progress is necessary for the default
implementation of `dslide-goto'.
⚠ Every sequence repeated calls to of `dslide-forward' should return nil
at some point or else infinite loops will result.")
(cl-defgeneric dslide-backward (obj)
"Advance OBJ backward by one step.
The return value has meaning to the deck:
- t: progress was made
- a point: progress was made up to a specific buffer location
- an org element: progress was made up to the :start property of
the element
- non-nil: ⚠ progress was made, but this value will warn because
the callee evedently returned something haphazardly
- nil: no progress could be made.
For sequences that don't make progress in a buffer, returning t is fine.
Returning a point of progress is necessary for the default
implementation of `dslide-goto'.
⚠ Every sequence of repeated calls to `dslide-backward' should return
nil at some point or else infinite loops will result.")
(cl-defgeneric dslide-goto (obj point)
"Advance OBJ forward beyond POINT.
This method can usually be implemented on top of `dslide-forward' by
advancing until POINT is exceeded. Return nil if POINT was not
exceeded. Return non-nil if the sense of progress exceeds POINT.
Usually, slide actions will be responsible for determining if the POINT
belongs to this slide or one of its child slides, and the slide will
just ask the child action.")
;; ** Stateful Sequence
(defclass dslide-stateful-sequence () ()
"An interface definition for linear sequences of steps.
This is an abstract class.
The sequence can be traversed forwards and backward. `begin' and
`foward' are conjugates of `end' and 'backward'.
Because the sequence steps may rely on some setup and should perform
necessary teardown, the stateful sequence provides `begin' `end' and
`final' methods.
It can also be indexed by high-level navigation commands. The
implementation of `dslide-goto' Sequences can run as sub-sequences,
where one sequence calls into another. 🚧 This capability is largely
unimplemented, but compatible with existing work.
Classes that wish to implement the stateful sequence interface just need
to support a few methods and then rely on the generic implementations
for the rest, unless they want to optimize or simplify their
implementation."
:abstract t)
(cl-defmethod dslide-begin ((_ dslide-stateful-sequence)))
(cl-defmethod dslide-end ((obj dslide-stateful-sequence))
(let ((progress t))
(while progress
(setq progress (dslide-forward obj)))))
(cl-defmethod dslide-forward ((_ dslide-stateful-sequence)))
(cl-defmethod dslide-backward ((_ dslide-stateful-sequence)))
(cl-defmethod dslide-final ((_ dslide-stateful-sequence)))
(cl-defmethod dslide-goto ((obj dslide-stateful-sequence) point)
(unless (eq 'skip (dslide-begin obj))
(let (exceeded (advanced t))
(while (and advanced (not exceeded))
(let ((progress (dslide-forward obj)))
(if (and (numberp progress)
(>= progress point))
(setq exceeded t)
(setq advanced progress)))))))
;; ** Parent
;; 🚧 this class is kind of half-baked. It was intended to wrap up the
;; filtering functionality and needing to find next and previous children.
;; Needs actual usage to become mature.
(defclass dslide-parent ()
((filter
:initform nil
:initarg :filter
:documentation "Function to filter child headings."))
"Class for objects that contain children.")
;; TODO unnecessary indirection
(cl-defmethod dslide-next-child ((obj dslide-parent) child)
"Get the next unfiltered CHILD of OBJ."
(dslide-next-sibling
child (oref obj filter)))
(cl-defmethod dslide-previous-child ((obj dslide-parent) child)
"Get the previous unfiltered CHILD of OBJ."
(dslide-previous-sibling
child (oref obj filter)))
;; ** Deck
;; TODO extract non-org-specific behavior to sequence-root class.
(defclass dslide-deck (dslide-parent)
((slide
:initform nil
:documentation "The active sequence or slide.
This is probably a `dslide-slide' object, but anything that implements
`dslide-stateful-sequence' will probably work as well.")
(base-buffer
:initform nil :initarg :base-buffer
:documentation "Source of the slide deck.")
(slide-buffer
:initform nil :initarg :slide-buffer
:documentation "Indirect buffer used to display slides in.")
(window-config
:initform nil :initarg :window-config
:documentation"Window configuration for restoring after stop.")
;; TODO the number of deck states is likely to grow with buffer tracking
;; moving into the deck object.
(slide-buffer-state
:initform nil
:documentation "Initiated by display actions to `contents' or `slides'.")
(step-callbacks
:initform nil
:documentation "Steps to run before next steps.
FORM is just a list as steps will always be run before any
sequence ends or makes progress.."))
"Root sequence that dispatches commands to slides.
Holds states such as those needed when switching between slides and
contents. Is responsible for picking root headings and hydrating them
into slides and their actions.
Class can be overridden to affect root behaviors. See
`dslide-default-deck-class'")
(defun dslide--slide-buffer-state (&optional update)
"Return or set the current slide buffer state.
Optional UPDATE sets the state.
🚧 It's unclear this needs a slot on the deck. Such decisions
are more relevant if running multiple decks becomes a support
goal again."
(if update
(oset dslide--deck slide-buffer-state update)
(when dslide--deck
(oref dslide--deck slide-buffer-state))))
(cl-defmethod dslide-begin ((obj dslide-deck))
"Initialize the first slide of OBJ."
(unless (oref obj slide)
;; Calls implied from other commands should have started the lifecycle already
(error "No slide selected"))
;; TODO This line is critical to starting up the state machine. Slides
;; are still inferring their need to narrow.
(narrow-to-region (point) (point)) ; signal to slide to draw itself
(dslide-begin (oref obj slide)))
(cl-defmethod dslide-end ((_ dslide-deck))
(error "Deck has no valid concept of starting at the end"))
(cl-defmethod dslide-final ((obj dslide-deck))
(when-let ((slide (oref obj slide)))
(dslide-final slide)))
;; Deck forward & backward methods are the entry point for user forward and
;; backward commands. They delegate out to slides, which may telescope into
;; their children in order to make progress.
;;
;; It make require several trips through the behavior to consume callbacks
;; that are run for effect or are no-op, things that don't count as steps or
;; are slides that decide at runtime to be skipped.
;;
;; In short, loop through whatever next steps and callbacks were pushed onto
;; the stack. When one of them makes progress, we're done.
(cl-defmethod dslide-forward ((obj dslide-deck))
(unless (oref obj slide)
;; Calls implied from other commands should have started the lifecycle
;; already
(error "No slide was set"))
(let ((inhibit-redisplay t)
(old-point-min (point-min))
progress
reached-end)
(while dslide--step-overlays
(delete-overlay (pop dslide--step-overlays)))
;; Burn up a step callback until one returns non-nil
(when-let ((steps (oref obj step-callbacks)))
(while (and steps (not progress))
(setq
progress
(let ((step (pop steps)))
(condition-case nil
(funcall step 'forward)
((debug error) (delay-warning
'(dslide dslide-step-callback)
"A step callback failed and was removed!"))))))
(oset obj step-callbacks steps))
(while (not (or progress reached-end))
(let* ((current-slide (oref obj slide))
(result (dslide-forward current-slide))
next-slide)
(if result
(setq progress result)
;; Check if there is a next sibling.
(if-let ((next-child (dslide-next-child obj current-slide)))
(setq next-slide next-child)
(setq reached-end t)))
(unless next-slide
(dslide--debug current-slide (format "forward: %s" progress)))
(when next-slide
(dslide--debug next-slide "switching to sibling")
(oset obj slide next-slide)
(dslide-final current-slide)
(dslide-begin next-slide)
;; Begin counts as a step
(setq progress next-slide))))
;; A lot of progress may have happened, but there will be only one feedback
;; message.
(when progress
(dslide--feedback :forward)
(dslide--follow progress (not (= old-point-min (point-min)))))
(when reached-end
(dslide--feedback :after-last-slide)
(run-hooks 'dslide-after-last-slide-hook))))
(cl-defmethod dslide-backward ((obj dslide-deck))
(unless (oref obj slide)
;; Calls implied from other commands should have started the lifecycle
;; already
(error "No slide was set"))
;; Going backward is almost the same as going forward. The big difference
;; is that when a slide is instantiated, it needs to be sent to its end.
;; Usually the default implementation, which calls forward until progress is
;; exhausted, is fine. Certain actions with side-effects may not like this,
;; and they should implement an actual `dslide-end' method as well as
;; idempotent `dslide-begin' and `dslide-final' if any support for going
;; backwards is desirable.
(let ((inhibit-redisplay t)
(old-point-min (point-min))
progress
reached-beginning)
(while dslide--step-overlays
(delete-overlay (pop dslide--step-overlays)))
;; Burn up a step callback until one returns non-nil
(when-let ((steps (oref obj step-callbacks)))
(while (and steps (not progress))
(setq
progress
(let ((step (pop steps)))
(condition-case nil
(funcall step 'backward)
((debug error) (delay-warning
'(dslide dslide-step-callback)
"A step callback failed and was removed!"))))))
(oset obj step-callbacks steps))
(while (not (or progress reached-beginning))
(let* ((current-slide (oref obj slide))
(result (dslide-backward current-slide))
previous-slide)
(if result
(setq progress result)
;; Check if there is a previous sibling.
(if-let ((previous-child (dslide-previous-child
obj current-slide)))
(setq previous-slide previous-child)
(setq reached-beginning t)))
(unless previous-slide
(dslide--debug current-slide (format "forward: %s" progress)))
(when previous-slide
(dslide--debug previous-slide "switching to sibling")
(oset obj slide previous-slide)
(dslide-final current-slide)
;; end counts as a step.
(dslide-end previous-slide)
(setq progress previous-slide))))
;; A lot of progress may have happened, but there will be only one feedback
;; message.
(cond (progress
(dslide--feedback :backward)
(dslide--follow progress (not (= old-point-min (point-min)))))
(reached-beginning
(user-error "No more previous slides!")))))
(cl-defmethod dslide--filter-function ((obj dslide-deck))
;; If the active slide has a more specific filter, return that
(or (oref (oref obj slide) filter)
(oref obj slide)))
(cl-defmethod dslide--choose-slide ((obj dslide-deck) how)
"Set the current slide of OBJ, according to HOW."
(let ((filter (oref obj filter)))
(pcase how
('first (oset obj slide (dslide--make-slide
(dslide--document-first-heading filter))))
('contents (oset obj slide (dslide--make-slide
(dslide--root-heading-at-point
filter (point)))))
('point
(let ((base-point (with-current-buffer (oref obj base-buffer)
(point))))
;; TODO implement looking inside the slides using `goto' and recover
;; the child with a point argument.
(oset obj slide
(dslide--make-slide
(dslide--root-heading-at-point filter base-point))))))))
;; TODO buffer states
(cl-defmethod dslide-deck-sane-p ((obj dslide-deck))
"Check if all of OBJ's buffers are alive or can be recovered."
(and (buffer-live-p (oref obj base-buffer))
(buffer-live-p (oref obj slide-buffer))
(eq (oref obj base-buffer) (buffer-base-buffer
(oref obj slide-buffer)))))
(cl-defmethod dslide--cleanup-step-callbacks ((obj dslide-deck))
"Run and dispose of all callbacks."
(mapc (lambda (f)
(condition-case nil
(funcall f nil)
((debug error) (delay-warning
'(dslide dslide-step-callback)
"A step callback failed in cleanup!"))))
(oref obj step-callbacks)))
(defun dslide-push-window-config (&optional step)
"Save the window configuration and narrowing for restoration.
Optional STEP argument will decide if the callback counts as a step or will
return nil so that it is only run for effects."
(let ((window-config (current-window-configuration)))
(dslide-push-step
(lambda (_) (prog1 step (set-window-configuration window-config))))))
(defun dslide-push-step (fun)
"Run FUN as next step.
FUN is a function of a single optional argument, `forward' or
`backward'. nil indicates that the callback is being cleaned up,
usually to quit the presentation or change to contents.
The return value is interpreted as progress, so return non-nil if you
want FUN to count as a step or nil if FUN is only run for effects.