https://github.com/positron-solutions/dslide/assets/73710933/06a66e42-a172-48ba-968f-5f5b1989a868
Fully programmable sequences behind a two-button interface:
dslide-deck-forward
dslide-deck-backward
Version 0.5.3 👷 Subscribe to Positron's YouTube for updates and related demonstrations.
The user-facing configuration API has been pretty unstable, but now that nested slide actions are supported, it is likely to remain roughly like it is now.
;; From MELPA
(use-package dslide)
;; package-vc
(package-vc-install
'(dslide
:url "https://github.com/positron-solutions/dslide.git"))
;; using elpaca's with explicit recipe
(use-package dslide
:elpaca (dslide :host github
:repo "positron-solutions/dslide"))
;; straight with explicit recipe
(use-package dslide
:straight (dslide :type git :host github
:repo "positron-solutions/dslide"))
;; or use manual load-path & require, you brave yak shaver
With just defaults, run dslide-deck-start
on your existing documents. You can load the examples in ./test/demo.org file to see a showcase of configuration behavior.
The default keymap uses arrow keys. Left and right are dslide-deck-forward
and dslide-deck-backward
. Up is dslide-deck-start
and will show the contents. Down is dslide-deck-stop
and will stop the slide show.
Call dslide-contents
to show a contents overview. Calling dslide-deck-forward
and dslide-deck-backward
in the contents can quickly move through headings. Call dslide-deck-start
again to resume the presentation from that point.
The actual display is done in an indirect buffer. Your hooks and customizations for presentation will not pollute your editing buffer. Dirty state will not pile up in your presentation buffer, greatly increasing reliability even if your custom Elisp scripting is sloppy 💩.
Check out dslide-deck-develop
. You can see the markup and the returned approximate progress indications. Babel actions will highlight blocks as they execute.
By default, the dslide-action-hide-markup
action is configured in dslide-default-actions
. Looks clean out of the box.
🚧 The current element hiding is implemented with overlays. I can be done with font-locking, but font-locking is better for less dynamic use cases.
dslide-deck-forward
and dslide-deck-backward
calls and implements lifecycle methods to initialize and clean up state
dslide-deck-foward
or dslide-deck-backward
, usually delegated down to dslide-forward
and dslide-backward
methodsdeck: my-presentation.org
buffer name.
slide-buffer
are used interchangeablybase-buffer
is used pretty exclusivelyBe sure to check M-x
customize-group
dslide
to see all declared custom variables. All of the variables are configured to recommended defaults except hooks, which would depend on other packages usually.
Many settings can be configured at:
You likely want to start the mode via dslide-deck-start
. Once the mode starts, it creates an indirect buffer to display the slides and then calls dslide-deck-start-function
once the mode is active and everything is initialized, so you can customize startup behavior.
All commands begin with dslide-deck
💡
(keymap-set org-mode-map "<f5>" #'dslide-deck-start)
Once the global minor mode, dslide-mode
is active, additional bindings in dslide-mode-map
are active in every buffer so that you can integrate other buffers into your presentation. (Tracking which buffers are part of a presentation is still a topic under consideration 🚧)
Because you might want to play a video or take a branch in the presentation and then exit that branch, the plan is to overload the dslide-deck-start
binding within presentations to enter / exit these branches.
Because slides and actions have a life-cycle and can easily find their own heading, consider making a custom action and setting that action on slides where it's needed.
Beware of using the normal dslide-mode-hook
😱 because it runs in the base buffer ⚠️. If you remap faces or add a bunch of styling, it will be copied to the indirect buffer but then linger in your base buffer. Instead, use dslide-start-hook
. 💡
dslide-start-hook
Is run in the indirect buffer after it is set it. This is what you want.dslide-stop-hook
is run in the base buffer because the indirect buffer is already dead.dslide-contents-hook
is run after switching to contents. It runs in the display buffer.dslide-narrow-hook
is run whenever a dslide-deck-forward
or dslide-deck-backward
changes the narrow state.dslide-after-last-slide-hook
is run when the user tries to go forward but there are no more slides. You can use this to implement a final feedback before quitting or add dslide-deck-stop
to exit without feedback.Another option is to use dslide-push-step
to push a callback that will only run when called going forward.
(defun my-stop-if-forward ()
(mc-push-step (lambda (direction)
(when (eq direction 'forward)
;; Be sure to return t or the hook will run again.
(prog1 t (dslide-deck-stop))))))
(setq dslide-after-last-slide-hook #'my-stop-if-forward)
Headings are treated as slides. Slides have actions. Actions are configured in the property drawer.
DSLIDE_SLIDE_ACTION
: Usually narrows to the slide and creates children from child headings. Lifecycle encloses the section.DSLIDE_ACTIONS:
Most commonly customized. You can list multiple actions. Each one will step through its forward and backward steps.Some actions must be fully enclosed by the lifecycle of a surrounding action, such as narrowing to the headline and section before displaying a contained list item-by-item.
🚧 Likely in the future, actions will be composable and accept arguments, using Lisp s-expressions. This API should be forward compatible.
Regular Org Mode markup is used to add actions to headings. See more examples in the <../test> directory.
* Full Screen Images
:PROPERTIES:
:DSLIDE_ACTIONS: dslide-action-images
:END:
#+attr_html: :width 50%
[[./images/emacsen4.jpeg]] [[./images/before-google3.jpeg]]
Many actions understand arguments, allowing tuning of similar behaviors from the same class. Implementing new arguments is relatively easy, just adding a slot and then reacting to the value of that slot.
Configuring the slot is done by adding plist-style properties after the class name:
:PROPERTIES:
:DSLIDE_ACTIONS: dslide-action-item-reveal :inline t
:END:
You can also use "property+" syntax to add to a property, and these accept plist arguments too:
:PROPERTIES:
:DSLIDE_ACTIONS: dslide-action-babel
:DSLIDE_ACTIONS+: dslide-action-images :fullscreen t
:END:
The deck and slide class as well as actions can all be sub-classed. Use the existing sub-classes of actions as example code for writing other classes. See the eieio#Top manual for explanation of OOP in Elisp.
If you suspect you might need to sub-class the dslide-slide
or dslide-deck
, please file an issue because your use case is probably interesting.
The dslide-section-next
and dslide-section-previous
method documentation are very helpful behavior for quickly writing custom actions. They advance the action's :marker
forwards and backwards to the next matching element and return that element so we can do something with it.
Example code:
(defclass dslide-action-red-paragraphs (dslide-action)
((overlays :initform nil))
"Paint the paragraphs red, one by one.")
;; Default no-op `dslide-begin' is sufficient
;; Default implementation of `dslide-end', which just plays forward to the end,
;; is well-behaved with this class.
;; Remove any remaining overlays when calling final.
(cl-defmethod dslide-final :after ((obj dslide-action-red-paragraphs))
(mapc #'delete-overlay (oref obj overlays)))
;; Find the next paragraph and add an overlay if it exists
(cl-defmethod dslide-forward ((obj dslide-action-red-paragraphs))
(when-let ((paragraph (dslide-section-next obj 'paragraph)))
(let* ((beg (org-element-property :begin paragraph))
(end (org-element-property :end paragraph))
(new-overlay (make-overlay beg end)))
(overlay-put new-overlay 'face 'error)
(push new-overlay (oref obj overlays))
;; Return non-nil to indicate progress was made. This also informs the
;; highlight when following the slides in the base buffer.
beg)))
(cl-defmethod dslide-backward ((obj dslide-action-red-paragraphs))
(when-let* ((overlay (pop (oref obj overlays))))
(delete-overlay overlay)
;; If there is a preceding overlay, move to its beginning else move to the
;; beginning of the heading.
(if-let ((overlay (car (oref obj overlays))))
(dslide-marker obj (overlay-start overlay))
(dslide-marker obj (org-element-property :begin (dslide-heading obj))))))
The default classes and actions can be configured at the document or customize level. Set the DSLIDE_DECK_CLASS
and DSLIDE_SLIDE_CLASS
as well as other properties that work at the heading level. The order of precedence (Not fully implemented 🚧):
You can write custom scripts into your presentation as Org Babel blocks. These can be executed with the dslide-action-babel
action. You just need to label your blocks with lifecycle methods if you want to be able to go forwards and backwards. See the dslide-action-babel
class and examples in <./test/demo.md>.
The #+attr_dslide:
affiliated keyword is used to configure which methods will run the block. Block labels that are understood:
begin
and end
are run when the slide is instantiated, going forward and backward respectively. You can have several blocks with these methods, and they will be run from top-to-bottom always, making it easier to re-use code usually.
final
is only called when no progress can be made or if the presentation is stopped.
forward
and backward
are self-explanatory. Position your backward
blocks above any block that they undo
both
runs either direction. It will not repeat in place when reversing. Use seperate forward
and backward
blocks for that 💡
See dslide-push-step
for inserting arbitrary callbacks that can function as steps. Unless your action performs state tracking to decide when to consume dslide-deck-forward
and dslide-deck-backward
itself, a callback may be easier.
Because babel blocks are not actions, using dslide-push-step
may be the only way to optionally add a step callback from a babel block.
This package is focused on creating a linear presentation sequence. For functionality not related to integrations into the dslide-deck-forward
dslide-deck-backward
interface, it is better to maintain separate packages and use hooks and babel scripting.
The master-of-ceremonies package contains utilities for display & presentation frame setup that are not specific to using DSL IDE.
mc-focus
. Check the full commands by pressing h
during focus. You can highlight a region, save an expression to playback a code snippet without the buffer open etc.hide the cursor or make it very subtle
;; Also check `mc-subtle-cursor-mode' (add-hook 'dslide-start-hook mc-hide-cursor-mode)
Sacha Chua has written an OBS plugin integration helpful for video integration obs-websocket-el.
orgit
can be used to show commits as links, which open with dslide-action-links
🚧 This is a lie. I was going to support this as a demonstration of a custom action.
The moom package contains some commands for resizing text and repositioning frames.
Bullets and many prettifications of common org markups. The markup that you don't hide looks better with org modern.
Never worry about turning on pretty links for a presentation. Edit them by just moving the point inside.
This is a description of how the pieces of the program must fit together. For any deep customization or hacking, the section is essential reading. At the least, it will greatly improve your success.
⚠️ Even if the current implementation differs, trust this domain model and expect the implementation to approach it. This section is pretty accurate as of 0.5.0
dslide-deck-forward
and dslide-deck-backward
is a concrete requirement that drives most of the rest of the implementation and feature design.This class is the heart of providing the common user interface and convenient implementation interface for extending the package.
The basis of all undo systems is either:
This is the command pattern. Navigating the linear sequence of a presentation is very similar to an undo system. Log-backed architectures such as git or event-sourcing can similarly be viewed as navigating to any point in a sequence by applying or rolling back a sequence of changes.
At the boundaries of a sequence of forward and reverse actions, it may be necessary to build up or tear down some state.
There are two setup methods:
dslide-begin
for setup going forwardsdslide-end
for setup going backwardsAdditionally, for teardown there is dslide-final
that is always called last, when the action or slide will be garbage collected and wants to clean up overlays etc.
In order to support contents based navigation, we need to be able to play a slide forward up to the current point. This may require instantiating some parent slides and playing them forward to a child. To avoid the need for parents to know about children, the dslide-goto
method was introduced.
The conclusion of the command pattern, setup & teardown, and indexing via point is the dslide-stateful-sequence
class. Anything that implements its interface can be controlled by dslide-deck-forward
and dslide-deck-backward
. The full interface:
dslide-begin
& dslide-end
dslide-final
dslide-forward
& dslide-backward
dslide-goto
- The default implementation of `dslide-end` is achieved by just walking forward from `dslide-begin`, calling `dslide-forward` until it returns `nil`.
- Implementing `dslide-goto` is optional as long as `dslide-begin` and `dslide-forward` can implement `dslide-end` and report their furthest extent of progress accurately.
- Ideally `dslide-deck-forward` & `dslide-deck-backward` along with `dslide-begin` & `dslide-end` form a closed system, but for the convenience of the implementer, it's fine to use an idempotent `dslide-begin` as the `dslide-deck-backward` step if granular backward is difficult or not valuable to implement.
Navigating a tree involves depth. Descendants may care about what happened in ancestors. Ancestors may care about what descendants leave behind. There may be conventions about what happens when descending into a child or returning from one.
At one time, slides were to be mostly independent and not running at the same time. While this simplified some things, it was limited.
Nesting slides and calling their actions might require updating several children concurrently. This was impossible to implement without pulling logic down into the parent slide's actions. Thus, the implementation calls through parents into children, sometimes calling several children.
The reason slide actions are distinct from other actions:
The lifetime of the slide action encompasses the section actions. It narrows or switches to a childe before the section actions attempt to work on the contents.
If something depends on something else existing or having been set up, its lifetime must be fully encompassed by that other thing. Especially since we are going forward & backward, setup & cleanup must happen on both ends of a sequence.
It is natural that a parent heading out-lives its child. User can take advantage of this by using the document or higher level headings to store state that needs to be shared by children. The final
calls for those things can call cleanup.
Actions live, for the most part, as long as the slide. Their dslide-begin
method is called at the very beginning. An action that reveals items must hide them before the user first sees them.
A consequence of this is that there are usually multiple actions alive at once. Something has to hold onto them. This is the slide.
Open issues and give feedback on feature requests. Contributions welcome.
When a slide is created in dslide-make-slide
, it can obtain them from several places:
The order of precedence and capability to override options is still pretty immature.
See the section about bindings for context. Video play or other situations where the presentation might branch should be supported by overloading the behavior of dslide-deck-start
. I think this command will turn into dslide-deck-secondary
in the dslide-mode-map
.
dslide-goto
, starting from pointSince not many actions currently have implemented this very accurately, playing from point is likely not that accurate. Progress updating in the base buffer is also currently only at the slide level of granularity.
There is no tracking whether a buffer is part of the presentation or not. How would a buffer become one? Should it be implicit? Without any sort of tracking, the consequence is that having a presentation open leaves the minor mode bindings hot. These commands do weird things when run from these situations, especially if running babel scripts, so some kind of first-class buffer affiliation seems necessary.
For terminals, the line-height based slide-in effect is not supported.
Sequences are often enclosed within other sequences, but there is currently no support for pushing or popping states when entering or exiting sequences. It's just not clear yet what cooperation might be necessary at sub-sequence boundaries.
There's no concrete reason why presentations need to start with Org mode buffers. The deck object could have its org-specific functionality pushed down to an org-mode class. The only requirement is to be able to hydrate some stateful sequences, which may hydrate and call into sub-sequences, meaning anything is pretty trivially possible.
This was not implemented yet, but evidently some had been filtering their headlines to only show TODO's in org-tree-slide
. Perhaps it is convenient to filter some tags and prevent them from being instantiated, especially if they will fail.
Especially if slides launch sub-sequences, and they do it from Lisp, this is hard. Buffer slides and also slide actions make it somewhat ambiguous. Counting trees or tracking the point might be easier. A children
method for sequences works as long as sequences actually implement it.
Children with no parents or missing a level are currently not supported and likely cause bad behavior.
This package is a direct descendant of Takaaki ISHIKAWA's org-tree-slide package. Many of the ideas and some of the implementations were either inherited or inspired by ideas from that package. This package would not exist without the inspiration. Thanks to everyone who contributed on org-tree-slide.