Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support speaker notes #389

Open
wants to merge 24 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
de42655
add speaker notes mode cli option
dmackdev Nov 4, 2024
fd6d57b
use ipc to navigate to specfic slides in speaker notes presentation w…
dmackdev Nov 5, 2024
3151fbd
process only speaker notes and end slide comments when in speaker not…
dmackdev Nov 5, 2024
cbadf07
fix layout on speaker note slides
dmackdev Nov 6, 2024
93a0dcb
move `SpeakerNotesChannel` to `Presenter` from `PresentationStateInner`
dmackdev Nov 8, 2024
22d70f7
restore original examples/code.md
dmackdev Nov 8, 2024
4c7726e
add speaker notes example presentation
dmackdev Nov 8, 2024
df051d3
show titles in speaker notes mode
dmackdev Nov 8, 2024
81cfbbc
remove ipc event service duplication
dmackdev Nov 8, 2024
4416af8
remove SpeakerNoteChannel enum
dmackdev Nov 11, 2024
5f781ac
bump CI rust version to 1.75.0
dmackdev Nov 11, 2024
0fd812e
Merge remote-tracking branch 'origin/master' into speaker-notes
dmackdev Nov 11, 2024
0f83b5a
split process_element for speaker notes and presentation modes
dmackdev Nov 11, 2024
dc0e039
fix formatting
dmackdev Nov 16, 2024
27a6151
use pubsub ipc messaging pattern instead of event
dmackdev Nov 16, 2024
43b44c5
incorporate presentation file name in ipc service name
dmackdev Nov 17, 2024
ddbaf41
split process comment command for speaker notes mode
dmackdev Nov 19, 2024
3114df9
propagate iceoryx2 publisher errors
dmackdev Nov 19, 2024
52d3f04
propagate iceoryx2 receiver error
dmackdev Nov 19, 2024
13e2d46
send new SpeakerNotesCommand::Exit ipc message to exit the speaker no…
dmackdev Nov 21, 2024
2964ac9
use string interpolation for ipc service name
dmackdev Nov 21, 2024
209cd10
propagate error instead of expect
dmackdev Nov 21, 2024
64dcc88
set iceoryx2 log level to error to hide misleading "missing config" w…
dmackdev Dec 4, 2024
7924524
add better error messages for iceoryx2 service open/create errors
dmackdev Dec 4, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
484 changes: 481 additions & 3 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ thiserror = "1"
unicode-width = "0.2"
os_pipe = "1.1.5"
libc = "0.2.155"
iceoryx2 = "0.4.1"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This requires rust 1.75 or newer, but CI runs rust 1.74.
A rust-toolchain file and Cargo.toml entry would help make this more visible.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any thoughts on bumping the rust version @mfontanini?
Otherwise a different version of iceoryx2 (if one exists for rust 1.74) or different IPC crate will need to be used.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also need to investigate some warnings and proper configuration for usage of this crate.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Go for it, 1.75 is almost a year old already


[dependencies.syntect]
version = "5.2"
Expand Down
42 changes: 42 additions & 0 deletions examples/speaker-notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
Speaker Notes
===

`presenterm` supports speaker notes.

You can use the following HTML comment throughout your presentation markdown file:

```markdown
<!-- speaker_note: Your speaker note goes here. -->
```

<!-- speaker_note: This is a speaker note from slide 1. -->

And you can run a separate instance of `presenterm` to view them.

<!-- speaker_note: You can use multiple speaker notes within each slide and interleave them with other markdown. -->

<!-- end_slide -->

Usage
===
Run the following two commands in separate terminals.

<!-- speaker_note: This is a speaker note from slide 2. -->

The `--speaker-notes-mode=publisher` argument will render your actual presentation as normal, without speaker notes:

```
presenterm --speaker-notes-mode=publisher examples/speaker-notes.md
```

The `--speaker-notes-mode=receiver` argument will render only the speaker notes for the current slide being shown in the actual presentation:

```
presenterm --speaker-notes-mode=receiver examples/speaker-notes.md
```

<!-- speaker_note: Demonstrate changing slides in the actual presentation. -->

As you change slides in your actual presentation, the speaker notes presentation slide will automatically navigate to the correct slide.

<!-- speaker_note: Isn't that cool? -->
2 changes: 1 addition & 1 deletion src/custom.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use crate::{
GraphicsMode,
input::user::KeyBinding,
media::{emulator::TerminalEmulator, kitty::KittyMode},
processing::code::SnippetLanguage,
GraphicsMode,
};
use clap::ValueEnum;
use schemars::JsonSchema;
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ pub use crate::{
input::source::CommandSource,
markdown::parse::MarkdownParser,
media::{graphics::GraphicsMode, printer::ImagePrinter, register::ImageRegistry},
presenter::{PresentMode, Presenter, PresenterOptions},
presenter::{PresentMode, Presenter, PresenterOptions, SpeakerNoteChannel},
processing::builder::{PresentationBuilderOptions, Themes},
render::highlighting::{CodeHighlighter, HighlightThemeSet},
resource::Resources,
Expand Down
53 changes: 48 additions & 5 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
use clap::{CommandFactory, Parser, error::ErrorKind};
use clap::{error::ErrorKind, CommandFactory, Parser, ValueEnum};
use comrak::Arena;
use directories::ProjectDirs;
use iceoryx2::{node::NodeBuilder, prelude::ServiceName, service::ipc};
use presenterm::{
CommandSource, Config, Exporter, GraphicsMode, HighlightThemeSet, ImagePrinter, ImageProtocol, ImageRegistry,
MarkdownParser, PresentMode, PresentationBuilderOptions, PresentationTheme, PresentationThemeSet, Presenter,
PresenterOptions, Resources, SnippetExecutor, Themes, ThemesDemo, ThirdPartyConfigs, ThirdPartyRender,
ValidateOverflows,
PresenterOptions, Resources, SnippetExecutor, SpeakerNoteChannel, Themes, ThemesDemo, ThirdPartyConfigs,
ThirdPartyRender, ValidateOverflows,
};
use schemars::JsonSchema;
use serde::Deserialize;
use std::{
env::{self, current_dir},
io,
Expand All @@ -17,6 +20,13 @@ use std::{

const DEFAULT_THEME: &str = "dark";

#[derive(Clone, Copy, Debug, Deserialize, ValueEnum, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum SpeakerNotesMode {
Publisher,
Receiver,
}

/// Run slideshows from your terminal.
#[derive(Parser)]
#[command()]
Expand Down Expand Up @@ -77,6 +87,9 @@ struct Cli {
/// The path to the configuration file.
#[clap(short, long)]
config_file: Option<String>,

#[clap(short, long)]
speaker_notes_mode: Option<SpeakerNotesMode>,
}

fn create_splash() -> String {
Expand Down Expand Up @@ -138,7 +151,12 @@ fn display_acknowledgements() {
println!("{}", String::from_utf8_lossy(acknowledgements));
}

fn make_builder_options(config: &Config, mode: &PresentMode, force_default_theme: bool) -> PresentationBuilderOptions {
fn make_builder_options(
config: &Config,
mode: &PresentMode,
force_default_theme: bool,
speaker_notes_mode: Option<SpeakerNotesMode>,
) -> PresentationBuilderOptions {
PresentationBuilderOptions {
allow_mutations: !matches!(mode, PresentMode::Export),
implicit_slide_ends: config.options.implicit_slide_ends.unwrap_or_default(),
Expand All @@ -151,6 +169,7 @@ fn make_builder_options(config: &Config, mode: &PresentMode, force_default_theme
strict_front_matter_parsing: config.options.strict_front_matter_parsing.unwrap_or(true),
enable_snippet_execution: config.snippet.exec.enable,
enable_snippet_execution_replace: config.snippet.exec_replace.enable,
render_speaker_notes_only: speaker_notes_mode.is_some_and(|mode| matches!(mode, SpeakerNotesMode::Receiver)),
}
}

Expand Down Expand Up @@ -189,6 +208,27 @@ fn overflow_validation(mode: &PresentMode, config: &ValidateOverflows) -> bool {
}
}

fn create_speaker_notes_channel(
speaker_notes_mode: SpeakerNotesMode,
) -> Result<SpeakerNoteChannel, Box<dyn std::error::Error>> {
let node = NodeBuilder::new().create::<ipc::Service>()?;
// TODO: Use a service name that incorporates presenterm and/or the presentation filename/title?
let service_name: ServiceName = "SpeakerNoteEventService".try_into()?;
let speaker_note_channel = match speaker_notes_mode {
SpeakerNotesMode::Publisher => {
let event = node.service_builder(&service_name).event().open_or_create()?;
let notifier = event.notifier_builder().create()?;
SpeakerNoteChannel::Notifier(notifier)
}
SpeakerNotesMode::Receiver => {
let event = node.service_builder(&service_name).event().open_or_create()?;
let listener = event.listener_builder().create()?;
SpeakerNoteChannel::Listener(listener)
}
};
Ok(speaker_note_channel)
}

fn run(mut cli: Cli) -> Result<(), Box<dyn std::error::Error>> {
if cli.generate_config_file_schema {
let schema = schemars::schema_for!(Config);
Expand Down Expand Up @@ -229,7 +269,7 @@ fn run(mut cli: Cli) -> Result<(), Box<dyn std::error::Error>> {
let parser = MarkdownParser::new(&arena);

let validate_overflows = overflow_validation(&mode, &config.defaults.validate_overflows) || cli.validate_overflows;
let mut options = make_builder_options(&config, &mode, force_default_theme);
let mut options = make_builder_options(&config, &mode, force_default_theme, cli.speaker_notes_mode);
if cli.enable_snippet_execution {
options.enable_snippet_execution = true;
}
Expand Down Expand Up @@ -273,6 +313,8 @@ fn run(mut cli: Cli) -> Result<(), Box<dyn std::error::Error>> {
let commands = CommandSource::new(config.bindings.clone())?;
options.print_modal_background = matches!(graphics_mode, GraphicsMode::Kitty { .. });

let speaker_notes_channel = cli.speaker_notes_mode.map(create_speaker_notes_channel).transpose()?;

let options = PresenterOptions {
builder_options: options,
mode,
Expand All @@ -290,6 +332,7 @@ fn run(mut cli: Cli) -> Result<(), Box<dyn std::error::Error>> {
themes,
printer,
options,
speaker_notes_channel,
);
presenter.present(&path)?;
}
Expand Down
38 changes: 36 additions & 2 deletions src/presenter.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
use iceoryx2::{
port::{listener::Listener, notifier::Notifier},
prelude::EventId,
service::ipc::Service,
};

use crate::{
custom::KeyBindingsConfig,
diff::PresentationDiffer,
Expand Down Expand Up @@ -37,6 +43,12 @@ pub struct PresenterOptions {
pub validate_overflows: bool,
}

#[derive(Debug)]
pub enum SpeakerNoteChannel {
Notifier(Notifier<Service>),
Listener(Listener<Service>),
}

/// A slideshow presenter.
///
/// This type puts everything else together.
Expand All @@ -52,6 +64,7 @@ pub struct Presenter<'a> {
image_printer: Arc<ImagePrinter>,
themes: Themes,
options: PresenterOptions,
speaker_notes_channel: Option<SpeakerNoteChannel>,
}

impl<'a> Presenter<'a> {
Expand All @@ -67,6 +80,7 @@ impl<'a> Presenter<'a> {
themes: Themes,
image_printer: Arc<ImagePrinter>,
options: PresenterOptions,
speaker_notes_channel: Option<SpeakerNoteChannel>,
) -> Self {
Self {
default_theme,
Expand All @@ -80,6 +94,7 @@ impl<'a> Presenter<'a> {
image_printer,
themes,
options,
speaker_notes_channel,
}
}

Expand All @@ -104,6 +119,13 @@ impl<'a> Presenter<'a> {
self.render(&mut drawer)?;

loop {
if let Some(SpeakerNoteChannel::Listener(listener)) = self.speaker_notes_channel.as_mut() {
if let Some(evt) = listener.try_wait_one().unwrap() {
self.apply_command(Command::GoToSlide(evt.as_value() as u32));
break;
}
}

if self.poll_async_renders()? {
self.render(&mut drawer)?;
}
Expand Down Expand Up @@ -136,6 +158,10 @@ impl<'a> Presenter<'a> {
CommandSideEffect::None => (),
};
}
if let Some(SpeakerNoteChannel::Notifier(notifier)) = self.speaker_notes_channel.as_mut() {
let current_slide_idx = self.state.presentation().current_slide_index();
notifier.notify_with_custom_event_id(EventId::new(current_slide_idx + 1)).unwrap();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you consider using the interprocess crate instead? I need to read iceoryx2's docs more but this API feels a bit strange. You're communicating via event ids, which is enough for what we're doing but it feels a bit off. (I admit I haven't looked at the docs enough to understand what events mean here).

Copy link
Contributor Author

@dmackdev dmackdev Nov 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No hard reason to keep using the event messaging pattern and EventId- iceoryx2 also supports publish/subscribe messaging pattern with different payloads (with restrictions). The examples in the readme/examples dir of the repo show publishing a usize. We could easily use this pattern instead. Semantically, I suppose publish/subscribe would be more appropriate.

Copy link
Contributor Author

@dmackdev dmackdev Nov 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had come across interprocess, but initially chose iceoryx2 since it appeared to be more popular and documented.
I have since tried the interprocess crate and had a quick go at using it in this branch but have not gotten that to work fully yet. Even running the local socket example from interprocess I noticed a few things: the "client" must strictly be started after the "server" otherwise it panics, and you have to manually remove the socket it creates between invocations, otherwise it panics.
So iceoryx2 seems to be more polished in this regard, and I think has a nicer interface.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair, interprocess would also require dealing with all the clients which is pretty annoying.

}
}
}

Expand Down Expand Up @@ -194,7 +220,11 @@ impl<'a> Presenter<'a> {
};
// If the screen is too small, simply ignore this. Eventually the user will resize the
// screen.
if matches!(result, Err(RenderError::TerminalTooSmall)) { Ok(()) } else { result }
if matches!(result, Err(RenderError::TerminalTooSmall)) {
Ok(())
} else {
result
}
}

fn apply_command(&mut self, command: Command) -> CommandSideEffect {
Expand Down Expand Up @@ -264,7 +294,11 @@ impl<'a> Presenter<'a> {
panic!("unreachable commands")
}
};
if needs_redraw { CommandSideEffect::Redraw } else { CommandSideEffect::None }
if needs_redraw {
CommandSideEffect::Redraw
} else {
CommandSideEffect::None
}
}

fn try_reload(&mut self, path: &Path, force: bool) {
Expand Down
Loading
Loading