-
Notifications
You must be signed in to change notification settings - Fork 34
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
base: master
Are you sure you want to change the base?
Conversation
add speaker note CommentCommand push text when processing speaker note comment
…hen the "publisher" presentation changes slides
There was a problem hiding this comment.
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.
@@ -36,6 +36,7 @@ thiserror = "1" | |||
unicode-width = "0.2" | |||
os_pipe = "1.1.5" | |||
libc = "0.2.155" | |||
iceoryx2 = "0.4.1" |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few comments mostly on whose responsibility this is but overall looks good!
examples/code.md
Outdated
@@ -6,14 +6,13 @@ theme: | |||
background: false | |||
--- | |||
|
|||
Code styling | |||
=== | |||
# Code styling |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not equivalent, I presume this was done by some prettifier? Can you roll back?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I think my editor extension did this - I will revert this and add a separate example presentation as you suggested, with consistent syntax.
examples/code.md
Outdated
@@ -35,10 +34,11 @@ fn main() { | |||
} | |||
``` | |||
|
|||
<!-- speaker_note: These are speaker notes on slide 1. --> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd create a separate example presentation on speaker notes specifically. I think the speaker notes require enough manual intervention to show up that I don't think it belongs in the main demo one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Started a new speaker notes presentation.
src/presentation.rs
Outdated
@@ -274,6 +289,7 @@ pub(crate) type AsyncPresentationErrorHolder = Arc<Mutex<Option<AsyncPresentatio | |||
pub(crate) struct PresentationStateInner { | |||
current_slide_index: usize, | |||
async_error_holder: AsyncPresentationErrorHolder, | |||
pub(crate) channel: Option<SpeakerNoteChannel>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This belongs in the presenter
type. The channel itself should be created outside and is not part of the presentation, it's part of "the app". e.g. you shouldn't have to re-set the channel every time the presentation is reloaded.
src/processing/builder.rs
Outdated
@@ -139,6 +143,26 @@ impl<'a> PresentationBuilder<'a> { | |||
bindings_config: KeyBindingsConfig, | |||
options: PresentationBuilderOptions, | |||
) -> Self { | |||
let presentation_state = PresentationState::default(); | |||
|
|||
if let Some(mode) = &options.speaker_notes_mode { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah like mentioned above, this should be handled externally to the presentation. Re-creating (even if it's trying to re-open an existing channel) the channels every time the presentation is built/reloaded doesn't really make sense.
src/processing/builder.rs
Outdated
if let Some(SpeakerNotesMode::Receiver) = self.options.speaker_notes_mode { | ||
match comment { | ||
CommentCommand::SpeakerNote(note) => { | ||
self.push_text(note.into(), ElementType::Paragraph); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the reason for this to be handled up here separately? Can't you handle SpeakerNote
in the match below and do the if let Some
inside the match arm?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I initially did this because, for the speaker notes presentation, only these two comments are handled: speaker notes, and end slide. The other comments (e.g. layout, pauses, etc) need not be applied to the speaker notes presentation.
In general, I think my additions to this file need to be refactored.
src/presenter.rs
Outdated
@@ -104,6 +115,11 @@ impl<'a> Presenter<'a> { | |||
self.render(&mut drawer)?; | |||
|
|||
loop { | |||
if let Some(idx) = self.state.presentation().listen_for_speaker_note_evt() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be interesting for this thing to be inside input/source.rs
and act as a normal command, e.g. it could return Command::GoToSlide
when such an event is found. This would make it more transparent to the presenter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would make sense - it is a source of a command after all.
So are you thinking:
- Remove the
SpeakerNoteChannel
enum, - Store the
Option<iceoryx2::Listener>
directly inCommandSource
if viewing speaker notes, - Store the
Option<iceoryx2::Notifier>
directly inPresenter
for the main presentation, if presenting with speaker notes.
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, exactly 👌
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
src/presentation.rs
Outdated
@@ -292,6 +308,22 @@ impl PresentationState { | |||
|
|||
fn set_current_slide_index(&self, value: usize) { | |||
self.inner.deref().borrow_mut().current_slide_index = value; | |||
if let Some(SpeakerNoteChannel::Notifier(notifier)) = &self.inner.deref().borrow().channel { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Following the idea of moving this to presenter, you can always emit this event after a command is handled. That would help detach this logic from out of here.
always notify channel of current slide index after command is applied pass bool flag to builder for rendering speaker notes only
support implicit slide ends in speaker notes mode
src/processing/builder.rs
Outdated
MarkdownElement::Comment { comment, source_position } => { | ||
self.process_comment(comment, source_position)? | ||
} | ||
MarkdownElement::SetexHeading { text } => self.push_slide_title(text), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking showing the slide title would be good for the speaker notes mode to help match it against the title of the current slide in the main presentation.
This also means we can support implicit slide ends in the speaker notes mode for free.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be good to have a separate process_element
for each mode so this one's not polluted with the logic from both. But I agree that having the heading makes sense 👌
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have split this. process_comment
could benefit from the same split, but there are extra processing steps/logic on the raw comment string.
Sorry I'm not having much free time until next week but I'll try to spend a bit of time I see activity in this PR. |
src/presenter.rs
Outdated
@@ -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(); |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
store IPC publisher in Presenter store IPC receiver in CommandSource
@@ -253,7 +263,7 @@ impl<'a> PresentationBuilder<'a> { | |||
self.push_line_break(); | |||
} | |||
|
|||
fn process_element(&mut self, element: MarkdownElement) -> Result<(), BuildError> { | |||
fn process_element_for_presentation_mode(&mut self, element: MarkdownElement) -> Result<(), BuildError> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably needs a better name so as to not be confused with PresentMode
.
Is there any reason why |
Looking at iceoryx2 I think you should really be using the pub/sub mode. The event one you're using doesn't make sense to me, like I said it's communicating via event ids which only works here because you're passing in a number but it won't work if this needs to be changed in the future. What I'd suggest is the following:
PS: the formatting needs nightly because most of the useful cargo formatting rules used here are nightly only. |
|
I see, makes sense, I forgot this uses shared memory so there really isn't a reason to serialize to JSON anyway. I think if there's a need to use strings in the future they can always be self contained re: naming makes sense as well, not sure if there are restrictions on the length of the service name. Using the file name should be enough. e.g. |
Updated the service name to incorporate the presentation file name. There does not appear to be any length restrictions on the service name, only that it cannot be empty (see here). From my side, I need to address a few |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a few comments mostly on error handling. I noticed when you run it it prints
2 [W] "Config::global_config()"
| Default config file found but unable to read data, populate config with default values.
```
Do you see this too? Not sure where it's coming from.
src/main.rs
Outdated
fn create_speaker_notes_service_builder( | ||
presentation_path: &Path, | ||
) -> Result<Builder<SpeakerNotesCommand, (), Service>, Box<dyn std::error::Error>> { | ||
let file_name = presentation_path.file_name().expect("failed to resolve presentation file name").to_string_lossy(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you not panic here and return an error instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've propagated a Cli::command().error(...)
here instead.
src/main.rs
Outdated
presentation_path: &Path, | ||
) -> Result<Builder<SpeakerNotesCommand, (), Service>, Box<dyn std::error::Error>> { | ||
let file_name = presentation_path.file_name().expect("failed to resolve presentation file name").to_string_lossy(); | ||
let service_name = format!("presenterm/{}", file_name).as_str().try_into()?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
format!("presenterm/{file_name}")
@@ -270,9 +305,21 @@ fn run(mut cli: Cli) -> Result<(), Box<dyn std::error::Error>> { | |||
println!("{}", serde_json::to_string_pretty(&meta)?); | |||
} | |||
} else { | |||
let commands = CommandSource::new(config.bindings.clone())?; | |||
let speaker_notes_event_receiver = if let Some(SpeakerNotesMode::Receiver) = cli.speaker_notes_mode { | |||
let receiver = create_speaker_notes_service_builder(&path)?.open()?.subscriber_builder().create()?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should fail with a nice looking error. I currently only see
PublishSubscribeOpenError::DoesNotExist
When I run the receiver before the publisher. Maybe use a Cli::command().error(...)
like above in this file with a meaningful error message like "no presenterm in publisher mode running" or something like that.
Same for the publisher failing when the channel is already open.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed.
There are several different error enums - each with multiple variants - involved in this chain of method calls, which makes this tricky to nicely isolate these particular variants.
I'll see what I can do.
@@ -0,0 +1,5 @@ | |||
#[derive(Debug)] | |||
#[repr(C)] | |||
pub enum SpeakerNotesCommand { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe there could be an Exit
command sent when presenterm exits? That way they both would close simultaneously.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea.
Yes I have been seeing that warning - I had mentioned it in the PR description. I think it would be a blocker for this PR since it is not an ideal user experience - any suggestions? |
Sorry, I saw that when you created the PR but forgot when trying it out.
Yyyyyeaaah I agree, I hope there's a release out soon. Do you have a link to the PR where this was removed? Depending on what they use we can potentially null out that output. |
Closes #112.
rust
version to 1.75.0 (for compatibility withiceoryx2
).Speaker notes implementation:
<!-- speaker_note: Your speaker note here. -->
.--speaker-notes-mode=publisher
CLI option is used to present slides. In this mode, speaker note comments are ignored and not rendered, and every time the slide changes, an IPC message is published.--speaker-notes-mode=receiver
is used to view speaker notes. In this mode, only the title of each slide and speaker note comments are rendered. When retrieving the next command, we first check for an IPC message to change slides.--speaker-notes-mode
is not specified, no IPC structures are created, no messages are published/listened for, and speaker notes are ignored and not rendered.Demo:
Screen.Recording.2024-11-19.at.19.21.07.mov
TODO:
Default config file found but unable to read data, populate config with default values.
is observed when exitingpresenterm
, but this has since been fixed on themain
branch.