diff --git a/.github/workflows/merge.yaml b/.github/workflows/merge.yaml index e793108..50862f1 100644 --- a/.github/workflows/merge.yaml +++ b/.github/workflows/merge.yaml @@ -15,7 +15,7 @@ jobs: uses: actions/checkout@v4 - name: Install rust toolchain - uses: dtolnay/rust-toolchain@1.74.0 + uses: dtolnay/rust-toolchain@1.75.0 - name: Run cargo check run: cargo check --features sixel @@ -28,7 +28,7 @@ jobs: uses: actions/checkout@v4 - name: Install rust toolchain - uses: dtolnay/rust-toolchain@1.74.0 + uses: dtolnay/rust-toolchain@1.75.0 - name: Run cargo test run: cargo test diff --git a/Cargo.lock b/Cargo.lock index ec48bb0..a91c951 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "aliasable" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -112,6 +118,29 @@ dependencies = [ "serde", ] +[[package]] +name = "bindgen" +version = "0.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "bitflags 2.6.0", + "cexpr", + "clang-sys", + "itertools 0.12.1", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", + "which", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -170,6 +199,25 @@ dependencies = [ "shlex", ] +[[package]] +name = "cdr" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9617422bf43fde9280707a7e90f8f7494389c182f5c70b0f67592d0f06d41dfa" +dependencies = [ + "byteorder", + "serde", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -189,6 +237,17 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" version = "4.5.20" @@ -217,7 +276,7 @@ version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "syn", @@ -464,6 +523,26 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca" +[[package]] +name = "enum-iterator" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c280b9e6b3ae19e152d8e31cf47f18389781e119d4013a2a2bb0180e5facc635" +dependencies = [ + "enum-iterator-derive", +] + +[[package]] +name = "enum-iterator-derive" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ab991c1362ac86c61ab6f556cff143daa22e5a15e4e189df818b2fd19fe65b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -569,6 +648,12 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "heck" version = "0.5.0" @@ -587,6 +672,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "iana-time-zone" version = "0.1.61" @@ -610,6 +704,206 @@ dependencies = [ "cc", ] +[[package]] +name = "iceoryx2" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2ff9bacac005d82f36638bc765d61658e424a21c5cb84d66ead7bd61cc7b373" +dependencies = [ + "cdr", + "iceoryx2-bb-container", + "iceoryx2-bb-derive-macros", + "iceoryx2-bb-elementary", + "iceoryx2-bb-lock-free", + "iceoryx2-bb-log", + "iceoryx2-bb-memory", + "iceoryx2-bb-posix", + "iceoryx2-bb-system-types", + "iceoryx2-cal", + "iceoryx2-pal-concurrency-sync", + "lazy_static", + "serde", + "sha1_smol", + "tiny-fn", + "toml", +] + +[[package]] +name = "iceoryx2-bb-container" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05da563f8303d069990b1a0c351f5431309c20fff0e52dbaf852cdaa6fc809d" +dependencies = [ + "iceoryx2-bb-derive-macros", + "iceoryx2-bb-elementary", + "iceoryx2-bb-log", + "iceoryx2-pal-concurrency-sync", + "serde", +] + +[[package]] +name = "iceoryx2-bb-derive-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efe33eee7e821e439fb5aa9bea95f9d45b90bf9554691908b46570a240c3d798" +dependencies = [ + "iceoryx2-bb-elementary", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "iceoryx2-bb-elementary" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee584a19f03c741f26851fe365fcb034ae745992e4604e2ad0d636a68422fb5" +dependencies = [ + "iceoryx2-pal-concurrency-sync", +] + +[[package]] +name = "iceoryx2-bb-lock-free" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a38f033bd95a8e915cc2e2c7a0a834d2eb07ffaa4aef8f3a32792a43c86a5ffc" +dependencies = [ + "iceoryx2-bb-elementary", + "iceoryx2-bb-log", + "iceoryx2-pal-concurrency-sync", + "tiny-fn", +] + +[[package]] +name = "iceoryx2-bb-log" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f7d8442ac92955b5055d4588218631b398ac94818452baf849857d7665959e6" +dependencies = [ + "iceoryx2-pal-concurrency-sync", + "termsize", +] + +[[package]] +name = "iceoryx2-bb-memory" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e07b76d3cfec24ba09237165d846418eabc0b5cc5be8840623089bcb236335" +dependencies = [ + "iceoryx2-bb-elementary", + "iceoryx2-bb-lock-free", + "iceoryx2-bb-log", + "iceoryx2-bb-posix", + "iceoryx2-pal-concurrency-sync", + "lazy_static", + "tiny-fn", +] + +[[package]] +name = "iceoryx2-bb-posix" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581ce58d22bd7ec58fa7f13056b454af33bdfbfecde97c21f013e36317683363" +dependencies = [ + "bitflags 2.6.0", + "enum-iterator", + "iceoryx2-bb-container", + "iceoryx2-bb-elementary", + "iceoryx2-bb-log", + "iceoryx2-bb-system-types", + "iceoryx2-pal-concurrency-sync", + "iceoryx2-pal-configuration", + "iceoryx2-pal-posix", + "lazy_static", + "serde", + "tiny-fn", +] + +[[package]] +name = "iceoryx2-bb-system-types" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3b8556076f9c96198465ab95d08e1aae2a54063a110b938651a465a2e075051" +dependencies = [ + "iceoryx2-bb-container", + "iceoryx2-bb-elementary", + "iceoryx2-bb-log", + "iceoryx2-pal-configuration", + "serde", +] + +[[package]] +name = "iceoryx2-bb-testing" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "257f890596e5bf7b7f44a281809d294bae430e4eed95620fc69b5770a40b2101" +dependencies = [ + "iceoryx2-pal-configuration", +] + +[[package]] +name = "iceoryx2-bb-threadsafe" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f6bdcfaa19bbcdb6ff556b836a9d73bb65563315e4c9cb6e865227e2b12bfd1" +dependencies = [ + "iceoryx2-bb-container", + "iceoryx2-bb-log", + "iceoryx2-bb-posix", +] + +[[package]] +name = "iceoryx2-cal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1aee45b9aee135b9b4a9923613754f1ae30acb44ed4ac9cf85fb3dc269651a7" +dependencies = [ + "cdr", + "iceoryx2-bb-container", + "iceoryx2-bb-elementary", + "iceoryx2-bb-lock-free", + "iceoryx2-bb-log", + "iceoryx2-bb-memory", + "iceoryx2-bb-posix", + "iceoryx2-bb-system-types", + "iceoryx2-bb-testing", + "iceoryx2-bb-threadsafe", + "iceoryx2-pal-concurrency-sync", + "lazy_static", + "once_cell", + "ouroboros", + "serde", + "sha1_smol", + "tiny-fn", + "toml", +] + +[[package]] +name = "iceoryx2-pal-concurrency-sync" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e255bb5b9f5c60968735531ab5797b467aaaf3c5fd53b10ebfbb81d5e436395e" + +[[package]] +name = "iceoryx2-pal-configuration" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9b61d9620508c53fc3fa31f821e5784d0c04abda7e46a2407b40a6a92f37ad0" + +[[package]] +name = "iceoryx2-pal-posix" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56672b845179f809997b396dc33267f5b759842e714edcc261d4adcd9d167c2a" +dependencies = [ + "bindgen", + "cc", + "iceoryx2-pal-concurrency-sync", + "iceoryx2-pal-configuration", + "lazy_static", + "windows-sys 0.48.0", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -672,6 +966,15 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.13.0" @@ -702,12 +1005,28 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "libc" version = "0.2.161" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" +[[package]] +name = "libloading" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" +dependencies = [ + "cfg-if", + "windows-targets 0.52.6", +] + [[package]] name = "libredox" version = "0.1.3" @@ -860,6 +1179,31 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "ouroboros" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "944fa20996a25aded6b4795c6d63f10014a7a83f8be9828a11860b08c5fc4a67" +dependencies = [ + "aliasable", + "ouroboros_macro", + "static_assertions", +] + +[[package]] +name = "ouroboros_macro" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39b0deead1528fd0e5947a8546a9642a9777c25f6e1e26f34c97b204bbb465bd" +dependencies = [ + "heck 0.4.1", + "itertools 0.12.1", + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn", +] + [[package]] name = "parking_lot" version = "0.12.3" @@ -944,8 +1288,9 @@ dependencies = [ "directories", "flate2", "hex", + "iceoryx2", "image", - "itertools", + "itertools 0.13.0", "libc", "merge-struct", "once_cell", @@ -967,6 +1312,16 @@ dependencies = [ "unicode-width 0.2.0", ] +[[package]] +name = "prettyplease" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.89" @@ -976,6 +1331,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "version_check", + "yansi", +] + [[package]] name = "quick-error" version = "2.0.1" @@ -1132,6 +1500,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc_version" version = "0.4.1" @@ -1254,6 +1628,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + [[package]] name = "serde_with" version = "3.11.0" @@ -1297,6 +1680,12 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + [[package]] name = "shlex" version = "1.3.0" @@ -1379,6 +1768,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.11.1" @@ -1400,7 +1795,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "rustversion", @@ -1452,6 +1847,16 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "termsize" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f11ff5c25c172608d5b85e2fb43ee9a6d683a7f4ab7f96ae07b3d8b590368fd" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "thiserror" version = "1.0.65" @@ -1503,6 +1908,12 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-fn" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fde9a76dac5751480f711f327371c809d7f8a9f036436e6237d67859adbf3bd" + [[package]] name = "tinyvec" version = "1.8.0" @@ -1524,6 +1935,40 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b130bd8a58c163224b44e217b4239ca7b927d82bf6cc2fea1fc561d15056e3f7" +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap 2.6.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "typed-arena" version = "2.0.2" @@ -1575,6 +2020,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "walkdir" version = "2.5.0" @@ -1652,6 +2103,18 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1840,6 +2303,21 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +dependencies = [ + "memchr", +] + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "zerocopy" version = "0.7.35" diff --git a/Cargo.toml b/Cargo.toml index 3c81516..5d31ce4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ thiserror = "1" unicode-width = "0.2" os_pipe = "1.1.5" libc = "0.2.155" +iceoryx2 = "0.4.1" [dependencies.syntect] version = "5.2" diff --git a/examples/speaker-notes.md b/examples/speaker-notes.md new file mode 100644 index 0000000..f579558 --- /dev/null +++ b/examples/speaker-notes.md @@ -0,0 +1,42 @@ +Speaker Notes +=== + +`presenterm` supports speaker notes. + +You can use the following HTML comment throughout your presentation markdown file: + +```markdown + +``` + + + +And you can run a separate instance of `presenterm` to view them. + + + + + +Usage +=== +Run the following two commands in separate terminals. + + + +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 +``` + + + +As you change slides in your actual presentation, the speaker notes presentation slide will automatically navigate to the correct slide. + + diff --git a/src/input/mod.rs b/src/input/mod.rs index 23060d5..b8954f9 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -1,2 +1,3 @@ pub(crate) mod source; +pub(crate) mod speaker_notes; pub(crate) mod user; diff --git a/src/input/source.rs b/src/input/source.rs index 19933ed..9e060be 100644 --- a/src/input/source.rs +++ b/src/input/source.rs @@ -1,7 +1,11 @@ -use super::user::{CommandKeyBindings, KeyBindingsValidationError, UserInput}; -use crate::custom::KeyBindingsConfig; +use super::{ + speaker_notes::SpeakerNotesCommand, + user::{CommandKeyBindings, KeyBindingsValidationError, UserInput}, +}; +use crate::{custom::KeyBindingsConfig, presenter::PresentationError}; +use iceoryx2::{port::subscriber::Subscriber, service::ipc::Service}; use serde::Deserialize; -use std::{io, time::Duration}; +use std::time::Duration; use strum::EnumDiscriminants; /// The source of commands. @@ -10,19 +14,33 @@ use strum::EnumDiscriminants; /// happens. pub struct CommandSource { user_input: UserInput, + speaker_notes_event_receiver: Option>, } impl CommandSource { /// Create a new command source over the given presentation path. - pub fn new(config: KeyBindingsConfig) -> Result { + pub fn new( + config: KeyBindingsConfig, + speaker_notes_event_receiver: Option>, + ) -> Result { let bindings = CommandKeyBindings::try_from(config)?; - Ok(Self { user_input: UserInput::new(bindings) }) + Ok(Self { user_input: UserInput::new(bindings), speaker_notes_event_receiver }) } /// Try to get the next command. /// /// This attempts to get a command and returns `Ok(None)` on timeout. - pub(crate) fn try_next_command(&mut self) -> io::Result> { + pub(crate) fn try_next_command(&mut self) -> Result, PresentationError> { + if let Some(receiver) = self.speaker_notes_event_receiver.as_mut() { + if let Some(msg) = receiver.receive()? { + match msg.payload() { + SpeakerNotesCommand::GoToSlide(idx) => { + return Ok(Some(Command::GoToSlide(*idx))); + } + SpeakerNotesCommand::Exit => return Ok(Some(Command::Exit)), + } + } + } match self.user_input.poll_next_command(Duration::from_millis(250))? { Some(command) => Ok(Some(command)), None => Ok(None), diff --git a/src/input/speaker_notes.rs b/src/input/speaker_notes.rs new file mode 100644 index 0000000..b01101d --- /dev/null +++ b/src/input/speaker_notes.rs @@ -0,0 +1,6 @@ +#[derive(Debug)] +#[repr(C)] +pub enum SpeakerNotesCommand { + GoToSlide(u32), + Exit, +} diff --git a/src/lib.rs b/src/lib.rs index 8d7a7c8..d5dfe53 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,7 +26,7 @@ pub use crate::{ demo::ThemesDemo, execute::SnippetExecutor, export::{ExportError, Exporter}, - input::source::CommandSource, + input::{source::CommandSource, speaker_notes::SpeakerNotesCommand}, markdown::parse::MarkdownParser, media::{graphics::GraphicsMode, printer::ImagePrinter, register::ImageRegistry}, presenter::{PresentMode, Presenter, PresenterOptions}, diff --git a/src/main.rs b/src/main.rs index d0aff1d..597c4e7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,21 @@ -use clap::{CommandFactory, Parser, error::ErrorKind}; +use clap::{CommandFactory, Parser, ValueEnum, error::ErrorKind}; use comrak::Arena; use directories::ProjectDirs; +use iceoryx2::{ + node::NodeBuilder, + service::{ + builder::publish_subscribe::{Builder, PublishSubscribeCreateError, PublishSubscribeOpenError}, + ipc::Service, + }, +}; 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, SpeakerNotesCommand, Themes, ThemesDemo, ThirdPartyConfigs, + ThirdPartyRender, ValidateOverflows, }; +use schemars::JsonSchema; +use serde::Deserialize; use std::{ env::{self, current_dir}, io, @@ -17,6 +26,41 @@ use std::{ const DEFAULT_THEME: &str = "dark"; +#[derive(Clone, Copy, Debug, Deserialize, ValueEnum, JsonSchema)] +#[serde(rename_all = "kebab-case")] +pub enum SpeakerNotesMode { + Publisher, + Receiver, +} + +#[derive(thiserror::Error, Debug)] +enum IpcServiceError { + #[error("no presenterm process in publisher mode running for presentation")] + ServiceOpenError, + #[error("existing presenterm process in publisher mode already running for presentation")] + ServiceCreateError, + #[error("{0}")] + Other(String), +} + +impl From for IpcServiceError { + fn from(value: PublishSubscribeOpenError) -> Self { + match value { + PublishSubscribeOpenError::DoesNotExist => Self::ServiceOpenError, + _ => Self::Other(value.to_string()), + } + } +} + +impl From for IpcServiceError { + fn from(value: PublishSubscribeCreateError) -> Self { + match value { + PublishSubscribeCreateError::AlreadyExists => Self::ServiceCreateError, + _ => Self::Other(value.to_string()), + } + } +} + /// Run slideshows from your terminal. #[derive(Parser)] #[command()] @@ -77,6 +121,9 @@ struct Cli { /// The path to the configuration file. #[clap(short, long)] config_file: Option, + + #[clap(short, long)] + speaker_notes_mode: Option, } fn create_splash() -> String { @@ -138,7 +185,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, +) -> PresentationBuilderOptions { PresentationBuilderOptions { allow_mutations: !matches!(mode, PresentMode::Export), implicit_slide_ends: config.options.implicit_slide_ends.unwrap_or_default(), @@ -151,6 +203,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)), } } @@ -189,6 +242,22 @@ fn overflow_validation(mode: &PresentMode, config: &ValidateOverflows) -> bool { } } +fn create_speaker_notes_service_builder( + presentation_path: &Path, +) -> Result, Box> { + let file_name = presentation_path + .file_name() + .ok_or(Cli::command().error(ErrorKind::InvalidValue, "failed to resolve presentation file name"))? + .to_string_lossy(); + let service_name = format!("presenterm/{file_name}").as_str().try_into()?; + let service = NodeBuilder::new() + .create::()? + .service_builder(&service_name) + .publish_subscribe::() + .max_publishers(1); + Ok(service) +} + fn run(mut cli: Cli) -> Result<(), Box> { if cli.generate_config_file_schema { let schema = schemars::schema_for!(Config); @@ -229,7 +298,7 @@ fn run(mut cli: Cli) -> Result<(), Box> { 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; } @@ -270,9 +339,29 @@ fn run(mut cli: Cli) -> Result<(), Box> { 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() + .map_err(|err| Cli::command().error(ErrorKind::InvalidValue, IpcServiceError::from(err)))? + .subscriber_builder() + .create()?; + Some(receiver) + } else { + None + }; + let commands = CommandSource::new(config.bindings.clone(), speaker_notes_event_receiver)?; options.print_modal_background = matches!(graphics_mode, GraphicsMode::Kitty { .. }); + let speaker_notes_event_publisher = if let Some(SpeakerNotesMode::Publisher) = cli.speaker_notes_mode { + let publisher = create_speaker_notes_service_builder(&path)? + .create() + .map_err(|err| Cli::command().error(ErrorKind::InvalidValue, IpcServiceError::from(err)))? + .publisher_builder() + .create()?; + Some(publisher) + } else { + None + }; let options = PresenterOptions { builder_options: options, mode, @@ -290,6 +379,7 @@ fn run(mut cli: Cli) -> Result<(), Box> { themes, printer, options, + speaker_notes_event_publisher, ); presenter.present(&path)?; } @@ -297,6 +387,7 @@ fn run(mut cli: Cli) -> Result<(), Box> { } fn main() { + iceoryx2::prelude::set_log_level(iceoryx2::prelude::LogLevel::Error); let cli = Cli::parse(); if let Err(e) = run(cli) { eprintln!("{e}"); diff --git a/src/presenter.rs b/src/presenter.rs index f7eef11..df1539a 100644 --- a/src/presenter.rs +++ b/src/presenter.rs @@ -1,4 +1,13 @@ +use iceoryx2::{ + port::{ + publisher::{Publisher, PublisherLoanError, PublisherSendError}, + subscriber::SubscriberReceiveError, + }, + service::ipc::Service, +}; + use crate::{ + SpeakerNotesCommand, custom::KeyBindingsConfig, diff::PresentationDiffer, execute::SnippetExecutor, @@ -52,6 +61,7 @@ pub struct Presenter<'a> { image_printer: Arc, themes: Themes, options: PresenterOptions, + speaker_notes_event_publisher: Option>, } impl<'a> Presenter<'a> { @@ -67,6 +77,7 @@ impl<'a> Presenter<'a> { themes: Themes, image_printer: Arc, options: PresenterOptions, + speaker_notes_event_publisher: Option>, ) -> Self { Self { default_theme, @@ -80,6 +91,7 @@ impl<'a> Presenter<'a> { image_printer, themes, options, + speaker_notes_event_publisher, } } @@ -121,7 +133,14 @@ impl<'a> Presenter<'a> { }, }; match self.apply_command(command) { - CommandSideEffect::Exit => return Ok(()), + CommandSideEffect::Exit => { + if let Some(publisher) = self.speaker_notes_event_publisher.as_mut() { + let sample = publisher.loan_uninit()?; + let sample = sample.write_payload(SpeakerNotesCommand::Exit); + sample.send()?; + } + return Ok(()); + } CommandSideEffect::Suspend => { self.suspend(&mut drawer); break; @@ -136,6 +155,12 @@ impl<'a> Presenter<'a> { CommandSideEffect::None => (), }; } + if let Some(publisher) = self.speaker_notes_event_publisher.as_mut() { + let current_slide_idx = self.state.presentation().current_slide_index() as u32; + let sample = publisher.loan_uninit()?; + let sample = sample.write_payload(SpeakerNotesCommand::GoToSlide(current_slide_idx + 1)); + sample.send()?; + } } } @@ -481,4 +506,13 @@ pub enum PresentationError { #[error("fatal error: {0}")] Fatal(String), + + #[error(transparent)] + SpeakerNotesPublisher(#[from] PublisherLoanError), + + #[error(transparent)] + SpeakerNotesSend(#[from] PublisherSendError), + + #[error(transparent)] + SpeakerNotesReceive(#[from] SubscriberReceiveError), } diff --git a/src/processing/builder.rs b/src/processing/builder.rs index 9449445..c8d816e 100644 --- a/src/processing/builder.rs +++ b/src/processing/builder.rs @@ -66,6 +66,7 @@ pub struct PresentationBuilderOptions { pub strict_front_matter_parsing: bool, pub enable_snippet_execution: bool, pub enable_snippet_execution_replace: bool, + pub render_speaker_notes_only: bool, } impl PresentationBuilderOptions { @@ -98,6 +99,7 @@ impl Default for PresentationBuilderOptions { strict_front_matter_parsing: true, enable_snippet_execution: false, enable_snippet_execution_replace: false, + render_speaker_notes_only: false, } } } @@ -179,7 +181,11 @@ impl<'a> PresentationBuilder<'a> { } for element in elements { self.slide_state.ignore_element_line_break = false; - self.process_element(element)?; + if self.options.render_speaker_notes_only { + self.process_element_for_speaker_notes_mode(element)?; + } else { + self.process_element_for_presentation_mode(element)?; + } self.validate_last_operation()?; if !self.slide_state.ignore_element_line_break { self.push_line_break(); @@ -253,7 +259,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> { let should_clear_last = !matches!(element, MarkdownElement::List(_) | MarkdownElement::Comment { .. }); match element { // This one is processed before everything else as it affects how the rest of the @@ -278,6 +284,18 @@ impl<'a> PresentationBuilder<'a> { Ok(()) } + fn process_element_for_speaker_notes_mode(&mut self, element: MarkdownElement) -> Result<(), BuildError> { + match element { + MarkdownElement::Comment { comment, source_position } => self.process_comment(comment, source_position)?, + MarkdownElement::SetexHeading { text } => self.push_slide_title(text), + _ => {} + } + // Allows us to start the next speaker slide when a title is pushed and implicit_slide_ends is enabled. + self.slide_state.last_element = LastElement::Other; + self.slide_state.ignore_element_line_break = true; + Ok(()) + } + fn process_front_matter(&mut self, contents: &str) -> Result<(), BuildError> { let metadata = match self.options.strict_front_matter_parsing { true => serde_yaml::from_str::(contents).map(PresentationMetadata::from), @@ -418,7 +436,16 @@ impl<'a> PresentationBuilder<'a> { Ok(comment) => comment, Err(error) => return Err(BuildError::CommandParse { line: source_position.start.line + 1, error }), }; - match comment { + + if self.options.render_speaker_notes_only { + self.process_comment_command_speaker_notes_mode(comment) + } else { + self.process_comment_command_presentation_mode(comment) + } + } + + fn process_comment_command_presentation_mode(&mut self, comment_command: CommentCommand) -> Result<(), BuildError> { + match comment_command { CommentCommand::Pause => self.process_pause(), CommentCommand::EndSlide => self.terminate_slide(), CommentCommand::NewLine => self.push_line_break(), @@ -458,12 +485,28 @@ impl<'a> PresentationBuilder<'a> { CommentCommand::NoFooter => { self.slide_state.ignore_footer = true; } + CommentCommand::SpeakerNote(_) => {} }; // Don't push line breaks for any comments. self.slide_state.ignore_element_line_break = true; Ok(()) } + fn process_comment_command_speaker_notes_mode( + &mut self, + comment_command: CommentCommand, + ) -> Result<(), BuildError> { + match comment_command { + CommentCommand::SpeakerNote(note) => { + self.push_text(note.into(), ElementType::Paragraph); + self.push_line_break(); + } + CommentCommand::EndSlide => self.terminate_slide(), + _ => {} + } + Ok(()) + } + fn should_ignore_comment(&self, comment: &str) -> bool { if comment.contains('\n') || !comment.starts_with(&self.options.command_prefix) { // Ignore any multi line comment; those are assumed to be user comments @@ -1145,6 +1188,7 @@ enum CommentCommand { JumpToMiddle, IncrementalLists(bool), NoFooter, + SpeakerNote(String), } impl FromStr for CommentCommand {