1
0
mirror of https://github.com/mfontanini/presenterm.git synced 2026-09-01 23:33:21 -06:00

Merge f42a2f4d12133f2add84472160cb7e7d69bce407 into 5f8add11a24af9d257fd18da48c8004dd9c516f8

This commit is contained in:
Benjamin Chausse 2026-07-30 01:18:34 -04:00 committed by GitHub
commit 935bba50db
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 80 additions and 4 deletions

View File

@ -10,6 +10,12 @@ defaults:
# the image protocol to use.
image_protocol: kitty-local
# whether to report presentation progress to the terminal via the OSC 9;4
# escape sequences. when enabled, terminals that support this protocol will
# show a progress bar in the window or tab title bar.
terminal_progress:
enable: false
typst:
# the pixels per inch when rendering latex/typst formulas.
ppi: 300

View File

@ -142,6 +142,10 @@ pub struct DefaultsConfig {
/// The configuration for tables when incremental tables are enabled.
#[serde(default)]
pub incremental_tables: IncrementalElementConfig,
/// Whether to report presentation progress to the terminal.
#[serde(default)]
pub terminal_progress: TerminalProgressConfig,
}
impl Default for DefaultsConfig {
@ -157,6 +161,7 @@ impl Default for DefaultsConfig {
max_rows_alignment: Default::default(),
incremental_lists: Default::default(),
incremental_tables: Default::default(),
terminal_progress: Default::default(),
}
}
}
@ -709,6 +714,15 @@ pub enum SlideTransitionStyleConfig {
CollapseHorizontal,
}
#[derive(Clone, Debug, Default, Deserialize)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct TerminalProgressConfig {
/// Whether to report presentation progress to the terminal via OSC 9;4 escape sequences.
#[serde(default)]
pub enable: bool,
}
fn make_keybindings<const N: usize>(raw_bindings: [&str; N]) -> Vec<KeyBinding> {
let mut bindings = Vec::new();
for binding in raw_bindings {

View File

@ -500,6 +500,7 @@ fn run(cli: Cli) -> Result<(), Box<dyn std::error::Error>> {
max_rows_alignment: config.defaults.max_rows_alignment,
},
transition: config.transition,
terminal_progress_enabled: config.defaults.terminal_progress.enable,
};
let presenter = Presenter::new(
&default_theme,

View File

@ -67,6 +67,11 @@ impl Presentation {
self.state.current_slide_index()
}
/// Get the total number of slides.
pub(crate) fn total_slides(&self) -> usize {
self.slides.len()
}
/// Jump forwards.
pub(crate) fn jump_next(&mut self) -> bool {
let current_slide = self.current_slide_mut();

View File

@ -52,6 +52,7 @@ pub struct PresenterOptions {
pub validate_overflows: bool,
pub max_size: MaxSize,
pub transition: Option<SlideTransitionConfig>,
pub terminal_progress_enabled: bool,
}
/// A slideshow presenter.
@ -70,6 +71,7 @@ pub struct Presenter<'a> {
options: PresenterOptions,
speaker_notes_event_publisher: Option<SpeakerNotesEventPublisher>,
poller: Poller,
terminal_progress_enabled: bool,
}
impl<'a> Presenter<'a> {
@ -87,6 +89,7 @@ impl<'a> Presenter<'a> {
options: PresenterOptions,
speaker_notes_event_publisher: Option<SpeakerNotesEventPublisher>,
) -> Self {
let terminal_progress_enabled = options.terminal_progress_enabled;
Self {
default_theme,
listener,
@ -100,6 +103,7 @@ impl<'a> Presenter<'a> {
options,
speaker_notes_event_publisher,
poller: Poller::launch(),
terminal_progress_enabled,
}
}
@ -114,8 +118,10 @@ impl<'a> Presenter<'a> {
let drawer_options = TerminalDrawerOptions {
font_size_fallback: self.options.font_size_fallback,
max_size: self.options.max_size.clone(),
terminal_progress_enabled: self.terminal_progress_enabled,
};
let mut drawer = TerminalDrawer::new(self.image_printer.clone(), drawer_options)?;
self.update_terminal_progress(&mut drawer);
loop {
// Poll async renders once before we draw just in case.
self.render(&mut drawer)?;
@ -167,6 +173,7 @@ impl<'a> Presenter<'a> {
}
if !matches!(self.state, PresenterState::Failure { .. }) {
self.update_terminal_progress(&mut drawer);
let slide_index = self.state.presentation().current_slide_index() as u32 + 1;
let slide = self.state.presentation().current_slide();
self.publish_event(SpeakerNotesEvent::GoTo {
@ -390,6 +397,20 @@ impl<'a> Presenter<'a> {
Ok(())
}
fn update_terminal_progress(&self, drawer: &mut TerminalDrawer) {
if !self.terminal_progress_enabled {
return;
}
let presentation = self.state.presentation();
let total = presentation.total_slides();
if total == 0 {
return;
}
let current = presentation.current_slide_index();
let percentage = ((current + 1) * 100 / total) as u8;
drawer.terminal.set_terminal_progress(percentage);
}
fn try_scale_transition_images(&self) -> RenderResult {
if self.options.transition.is_none() {
return Ok(());

View File

@ -36,11 +36,12 @@ pub(crate) type RenderResult = Result<(), RenderError>;
pub(crate) struct TerminalDrawerOptions {
pub(crate) font_size_fallback: u8,
pub(crate) max_size: MaxSize,
pub(crate) terminal_progress_enabled: bool,
}
impl Default for TerminalDrawerOptions {
fn default() -> Self {
Self { font_size_fallback: 1, max_size: Default::default() }
Self { font_size_fallback: 1, max_size: Default::default(), terminal_progress_enabled: false }
}
}
@ -52,7 +53,7 @@ pub(crate) struct TerminalDrawer {
impl TerminalDrawer {
pub(crate) fn new(image_printer: Arc<ImagePrinter>, options: TerminalDrawerOptions) -> io::Result<Self> {
let terminal = Terminal::new(io::stdout(), image_printer)?;
let terminal = Terminal::new(io::stdout(), image_printer, options.terminal_progress_enabled)?;
Ok(Self { terminal, options })
}

View File

@ -17,7 +17,7 @@ impl OverflowValidator {
let printer = Arc::new(ImagePrinter::Null);
for (index, slide) in presentation.iter_slides().enumerate() {
let index = index + 1;
let mut terminal = Terminal::new(io::Empty::default(), printer.clone()).map_err(RenderError::from)?;
let mut terminal = Terminal::new(io::Empty::default(), printer.clone(), false).map_err(RenderError::from)?;
let options = RenderEngineOptions { validate_overflows: true, ..Default::default() };
let engine = RenderEngine::new(&mut terminal, dimensions, options);
match engine.render(slide.iter_visible_operations()) {

View File

@ -59,10 +59,12 @@ pub(crate) struct Terminal<I: TerminalWrite> {
last_cleared_background_color: Option<Color>,
background_color: Option<Color>,
osc11_background: bool,
terminal_progress_enabled: bool,
terminal_progress: u8,
}
impl<I: TerminalWrite> Terminal<I> {
pub(crate) fn new(mut writer: I, image_printer: Arc<ImagePrinter>) -> io::Result<Self> {
pub(crate) fn new(mut writer: I, image_printer: Arc<ImagePrinter>, terminal_progress_enabled: bool) -> io::Result<Self> {
writer.init()?;
Ok(Self {
writer,
@ -74,6 +76,8 @@ impl<I: TerminalWrite> Terminal<I> {
background_color: None,
// Only use OSC11 when outside of tmux temporarily since it somehow breaks under kitty
osc11_background: !TerminalEmulator::capabilities().tmux,
terminal_progress_enabled,
terminal_progress: 0,
})
}
@ -193,12 +197,35 @@ impl<I: TerminalWrite> Terminal<I> {
Ok(())
}
pub(crate) fn set_terminal_progress(&mut self, percentage: u8) {
if !self.terminal_progress_enabled {
return;
}
self.terminal_progress = percentage;
let _ = write!(self.writer, "\x1b]9;4;1;{percentage}\x1b\\");
let _ = self.writer.flush();
}
pub(crate) fn clear_terminal_progress(&mut self) {
if !self.terminal_progress_enabled {
return;
}
self.terminal_progress = 0;
let _ = write!(self.writer, "\x1b]9;4;0\x1b\\");
let _ = self.writer.flush();
}
pub(crate) fn suspend(&mut self) {
self.clear_terminal_progress();
self.writer.deinit();
}
pub(crate) fn resume(&mut self) {
let _ = self.writer.init();
if self.terminal_progress_enabled && self.terminal_progress > 0 {
let _ = write!(self.writer, "\x1b]9;4;1;{}\x1b\\", self.terminal_progress);
let _ = self.writer.flush();
}
}
}
@ -233,6 +260,7 @@ impl<I: TerminalWrite> TerminalIo for Terminal<I> {
impl<I: TerminalWrite> Drop for Terminal<I> {
fn drop(&mut self) {
self.clear_terminal_progress();
if self.osc11_background {
if let Some(Color::Rgb { .. }) = self.background_color {
let _ = write!(self.writer, "\x1b]111\x1b\\");