mirror of
https://github.com/mfontanini/presenterm.git
synced 2025-05-05 15:32:58 +00:00
chore: refactor async renders (#556)
This refactors how async renders work. Before this change, the presenter had to periodically poll them to pull their state into the operation, and had to figure out which slides had async renders to be able to poll them at the right times. This was tedious and error prone, especially once slide transitions were introduced: we now had to preemptively poll the next/previous slide because there could be something being async rendered (e.g. a mermaid diagram) and we didn't want to transition into a slide with a "Loading..." text when the generated image was available, just not polled yet. This now moves all polling to a separate thread. When an operation needs to be polled (be it because it automatically starts async rendering or it requires to be triggered by pressing `<c-e>`), now a `Pollable` type is created that is essentially a container for the logic to poll and a state shared with the operation. This lets a `Poller` periodically poll all `Pollables` that need polling (lols). The result is a lot less code around `Presenter`/`Presentation` to deal with triggering and polling async renders. Also the handling for async render errors is now much nicer because it can be treated almost the same way as a successfully finished async render.
This commit is contained in:
commit
aa7cdae105
@ -36,7 +36,7 @@ impl CommandListener {
|
|||||||
return Ok(Some(command));
|
return Ok(Some(command));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
match self.keyboard.poll_next_command(Duration::from_millis(250))? {
|
match self.keyboard.poll_next_command(Duration::from_millis(100))? {
|
||||||
Some(command) => Ok(Some(command)),
|
Some(command) => Ok(Some(command)),
|
||||||
None => Ok(None),
|
None => Ok(None),
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ use crate::{
|
|||||||
terminal::emulator::TerminalEmulator,
|
terminal::emulator::TerminalEmulator,
|
||||||
theme::raw::PresentationTheme,
|
theme::raw::PresentationTheme,
|
||||||
};
|
};
|
||||||
use std::{io, rc::Rc};
|
use std::{io, sync::Arc};
|
||||||
|
|
||||||
const PRESENTATION: &str = r#"
|
const PRESENTATION: &str = r#"
|
||||||
# Header 1
|
# Header 1
|
||||||
@ -109,7 +109,7 @@ impl ThemesDemo {
|
|||||||
theme_options: ThemeOptions { font_size_supported: TerminalEmulator::capabilities().font_size },
|
theme_options: ThemeOptions { font_size_supported: TerminalEmulator::capabilities().font_size },
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let executer = Rc::new(SnippetExecutor::default());
|
let executer = Arc::new(SnippetExecutor::default());
|
||||||
let bindings_config = Default::default();
|
let bindings_config = Default::default();
|
||||||
let builder = PresentationBuilder::new(
|
let builder = PresentationBuilder::new(
|
||||||
theme,
|
theme,
|
||||||
|
@ -5,12 +5,13 @@ use crate::{
|
|||||||
export::pdf::PdfRender,
|
export::pdf::PdfRender,
|
||||||
markdown::{parse::ParseError, text_style::Color},
|
markdown::{parse::ParseError, text_style::Color},
|
||||||
presentation::{
|
presentation::{
|
||||||
Presentation, Slide,
|
Presentation,
|
||||||
builder::{BuildError, PresentationBuilder, PresentationBuilderOptions, Themes},
|
builder::{BuildError, PresentationBuilder, PresentationBuilderOptions, Themes},
|
||||||
|
poller::{Poller, PollerCommand},
|
||||||
},
|
},
|
||||||
render::{
|
render::{
|
||||||
RenderError,
|
RenderError,
|
||||||
operation::{AsRenderOperations, RenderAsyncState, RenderOperation},
|
operation::{AsRenderOperations, PollableState, RenderOperation},
|
||||||
properties::WindowSize,
|
properties::WindowSize,
|
||||||
},
|
},
|
||||||
theme::{ProcessingThemeError, raw::PresentationTheme},
|
theme::{ProcessingThemeError, raw::PresentationTheme},
|
||||||
@ -28,8 +29,7 @@ use std::{
|
|||||||
fs, io,
|
fs, io,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
thread::sleep,
|
sync::Arc,
|
||||||
time::Duration,
|
|
||||||
};
|
};
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|
||||||
@ -63,7 +63,7 @@ pub struct Exporter<'a> {
|
|||||||
default_theme: &'a PresentationTheme,
|
default_theme: &'a PresentationTheme,
|
||||||
resources: Resources,
|
resources: Resources,
|
||||||
third_party: ThirdPartyRender,
|
third_party: ThirdPartyRender,
|
||||||
code_executor: Rc<SnippetExecutor>,
|
code_executor: Arc<SnippetExecutor>,
|
||||||
themes: Themes,
|
themes: Themes,
|
||||||
dimensions: WindowSize,
|
dimensions: WindowSize,
|
||||||
options: PresentationBuilderOptions,
|
options: PresentationBuilderOptions,
|
||||||
@ -77,7 +77,7 @@ impl<'a> Exporter<'a> {
|
|||||||
default_theme: &'a PresentationTheme,
|
default_theme: &'a PresentationTheme,
|
||||||
resources: Resources,
|
resources: Resources,
|
||||||
third_party: ThirdPartyRender,
|
third_party: ThirdPartyRender,
|
||||||
code_executor: Rc<SnippetExecutor>,
|
code_executor: Arc<SnippetExecutor>,
|
||||||
themes: Themes,
|
themes: Themes,
|
||||||
mut options: PresentationBuilderOptions,
|
mut options: PresentationBuilderOptions,
|
||||||
mut dimensions: WindowSize,
|
mut dimensions: WindowSize,
|
||||||
@ -129,9 +129,7 @@ impl<'a> Exporter<'a> {
|
|||||||
|
|
||||||
let mut render = PdfRender::new(self.dimensions, output_directory);
|
let mut render = PdfRender::new(self.dimensions, output_directory);
|
||||||
Self::log("waiting for images to be generated and code to be executed, if any...")?;
|
Self::log("waiting for images to be generated and code to be executed, if any...")?;
|
||||||
for slide in presentation.iter_slides_mut() {
|
Self::render_async_images(&mut presentation);
|
||||||
Self::render_async_images(slide);
|
|
||||||
}
|
|
||||||
for (index, slide) in presentation.into_slides().into_iter().enumerate() {
|
for (index, slide) in presentation.into_slides().into_iter().enumerate() {
|
||||||
let index = index + 1;
|
let index = index + 1;
|
||||||
Self::log(&format!("processing slide {index}..."))?;
|
Self::log(&format!("processing slide {index}..."))?;
|
||||||
@ -154,24 +152,33 @@ impl<'a> Exporter<'a> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_async_images(slide: &mut Slide) {
|
fn render_async_images(presentation: &mut Presentation) {
|
||||||
for op in slide.iter_operations_mut() {
|
let poller = Poller::launch();
|
||||||
if let RenderOperation::RenderAsync(inner) = op {
|
let mut pollables = Vec::new();
|
||||||
loop {
|
for (index, slide) in presentation.iter_slides().enumerate() {
|
||||||
match inner.poll_state() {
|
for op in slide.iter_operations() {
|
||||||
RenderAsyncState::Rendering { .. } => {
|
if let RenderOperation::RenderAsync(inner) = op {
|
||||||
sleep(Duration::from_millis(200));
|
// Send a pollable to the poller and keep one for ourselves.
|
||||||
continue;
|
poller.send(PollerCommand::Poll { pollable: inner.pollable(), slide: index });
|
||||||
}
|
pollables.push(inner.pollable())
|
||||||
RenderAsyncState::Rendered | RenderAsyncState::JustFinishedRendering => break,
|
}
|
||||||
RenderAsyncState::NotStarted => inner.start_render(),
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
// Poll until they're all done
|
||||||
|
for mut pollable in pollables {
|
||||||
|
while let PollableState::Unmodified | PollableState::Modified = pollable.poll() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace render asyncs with new operations that contains the replaced image
|
||||||
|
// and any other unmodified operations.
|
||||||
|
for slide in presentation.iter_slides_mut() {
|
||||||
|
for op in slide.iter_operations_mut() {
|
||||||
|
if let RenderOperation::RenderAsync(inner) = op {
|
||||||
|
let window_size = WindowSize { rows: 0, columns: 0, width: 0, height: 0 };
|
||||||
|
let new_operations = inner.as_render_operations(&window_size);
|
||||||
|
*op = RenderOperation::RenderDynamic(Rc::new(RenderMany(new_operations)));
|
||||||
}
|
}
|
||||||
let window_size = WindowSize { rows: 0, columns: 0, width: 0, height: 0 };
|
|
||||||
let new_operations = inner.as_render_operations(&window_size);
|
|
||||||
// Replace this operation with a new operation that contains the replaced image
|
|
||||||
// and any other unmodified operations.
|
|
||||||
*op = RenderOperation::RenderDynamic(Rc::new(RenderMany(new_operations)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,6 @@ use std::{
|
|||||||
env::{self, current_dir},
|
env::{self, current_dir},
|
||||||
io,
|
io,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
rc::Rc,
|
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
use terminal::emulator::TerminalEmulator;
|
use terminal::emulator::TerminalEmulator;
|
||||||
@ -192,7 +191,7 @@ impl Customizations {
|
|||||||
|
|
||||||
struct CoreComponents {
|
struct CoreComponents {
|
||||||
third_party: ThirdPartyRender,
|
third_party: ThirdPartyRender,
|
||||||
code_executor: Rc<SnippetExecutor>,
|
code_executor: Arc<SnippetExecutor>,
|
||||||
resources: Resources,
|
resources: Resources,
|
||||||
printer: Arc<ImagePrinter>,
|
printer: Arc<ImagePrinter>,
|
||||||
builder_options: PresentationBuilderOptions,
|
builder_options: PresentationBuilderOptions,
|
||||||
@ -242,7 +241,7 @@ impl CoreComponents {
|
|||||||
threads: config.snippet.render.threads,
|
threads: config.snippet.render.threads,
|
||||||
};
|
};
|
||||||
let third_party = ThirdPartyRender::new(third_party_config, registry, &resources_path);
|
let third_party = ThirdPartyRender::new(third_party_config, registry, &resources_path);
|
||||||
let code_executor = Rc::new(code_executor);
|
let code_executor = Arc::new(code_executor);
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
third_party,
|
third_party,
|
||||||
code_executor,
|
code_executor,
|
||||||
|
@ -40,7 +40,7 @@ use comrak::{Arena, nodes::AlertType};
|
|||||||
use image::DynamicImage;
|
use image::DynamicImage;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use snippet::{SnippetOperations, SnippetProcessor, SnippetProcessorState};
|
use snippet::{SnippetOperations, SnippetProcessor, SnippetProcessorState};
|
||||||
use std::{collections::HashSet, fmt::Display, iter, mem, path::PathBuf, rc::Rc, str::FromStr};
|
use std::{collections::HashSet, fmt::Display, iter, mem, path::PathBuf, rc::Rc, str::FromStr, sync::Arc};
|
||||||
use unicode_width::UnicodeWidthStr;
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
mod snippet;
|
mod snippet;
|
||||||
@ -125,7 +125,7 @@ pub(crate) struct PresentationBuilder<'a> {
|
|||||||
chunk_mutators: Vec<Box<dyn ChunkMutator>>,
|
chunk_mutators: Vec<Box<dyn ChunkMutator>>,
|
||||||
slide_builders: Vec<SlideBuilder>,
|
slide_builders: Vec<SlideBuilder>,
|
||||||
highlighter: SnippetHighlighter,
|
highlighter: SnippetHighlighter,
|
||||||
code_executor: Rc<SnippetExecutor>,
|
code_executor: Arc<SnippetExecutor>,
|
||||||
theme: PresentationTheme,
|
theme: PresentationTheme,
|
||||||
default_raw_theme: &'a raw::PresentationTheme,
|
default_raw_theme: &'a raw::PresentationTheme,
|
||||||
resources: Resources,
|
resources: Resources,
|
||||||
@ -148,7 +148,7 @@ impl<'a> PresentationBuilder<'a> {
|
|||||||
default_raw_theme: &'a raw::PresentationTheme,
|
default_raw_theme: &'a raw::PresentationTheme,
|
||||||
resources: Resources,
|
resources: Resources,
|
||||||
third_party: &'a mut ThirdPartyRender,
|
third_party: &'a mut ThirdPartyRender,
|
||||||
code_executor: Rc<SnippetExecutor>,
|
code_executor: Arc<SnippetExecutor>,
|
||||||
themes: &'a Themes,
|
themes: &'a Themes,
|
||||||
image_registry: ImageRegistry,
|
image_registry: ImageRegistry,
|
||||||
bindings_config: KeyBindingsConfig,
|
bindings_config: KeyBindingsConfig,
|
||||||
@ -934,11 +934,9 @@ impl<'a> PresentationBuilder<'a> {
|
|||||||
image_registry: &self.image_registry,
|
image_registry: &self.image_registry,
|
||||||
snippet_executor: self.code_executor.clone(),
|
snippet_executor: self.code_executor.clone(),
|
||||||
theme: &self.theme,
|
theme: &self.theme,
|
||||||
presentation_state: &self.presentation_state,
|
|
||||||
third_party: self.third_party,
|
third_party: self.third_party,
|
||||||
highlighter: &self.highlighter,
|
highlighter: &self.highlighter,
|
||||||
options: &self.options,
|
options: &self.options,
|
||||||
slide_number: self.slide_builders.len() + 1,
|
|
||||||
font_size: self.slide_font_size(),
|
font_size: self.slide_font_size(),
|
||||||
};
|
};
|
||||||
let processor = SnippetProcessor::new(state);
|
let processor = SnippetProcessor::new(state);
|
||||||
@ -1174,6 +1172,7 @@ pub enum BuildError {
|
|||||||
InvalidFooter(#[from] InvalidFooterTemplateError),
|
InvalidFooter(#[from] InvalidFooterTemplateError),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
enum ExecutionMode {
|
enum ExecutionMode {
|
||||||
AlongSnippet,
|
AlongSnippet,
|
||||||
ReplaceSnippet,
|
ReplaceSnippet,
|
||||||
@ -1371,7 +1370,7 @@ mod test {
|
|||||||
let tmp_dir = std::env::temp_dir();
|
let tmp_dir = std::env::temp_dir();
|
||||||
let resources = Resources::new(&tmp_dir, &tmp_dir, Default::default());
|
let resources = Resources::new(&tmp_dir, &tmp_dir, Default::default());
|
||||||
let mut third_party = ThirdPartyRender::default();
|
let mut third_party = ThirdPartyRender::default();
|
||||||
let code_executor = Rc::new(SnippetExecutor::default());
|
let code_executor = Arc::new(SnippetExecutor::default());
|
||||||
let themes = Themes::default();
|
let themes = Themes::default();
|
||||||
let bindings = KeyBindingsConfig::default();
|
let bindings = KeyBindingsConfig::default();
|
||||||
let builder = PresentationBuilder::new(
|
let builder = PresentationBuilder::new(
|
||||||
|
@ -10,31 +10,29 @@ use crate::{
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
markdown::elements::SourcePosition,
|
markdown::elements::SourcePosition,
|
||||||
presentation::{ChunkMutator, PresentationState},
|
presentation::ChunkMutator,
|
||||||
render::{
|
render::{
|
||||||
operation::{AsRenderOperations, RenderAsync, RenderOperation},
|
operation::{AsRenderOperations, RenderAsyncStartPolicy, RenderOperation},
|
||||||
properties::WindowSize,
|
properties::WindowSize,
|
||||||
},
|
},
|
||||||
resource::Resources,
|
resource::Resources,
|
||||||
theme::{CodeBlockStyle, PresentationTheme},
|
theme::{CodeBlockStyle, PresentationTheme},
|
||||||
third_party::{ThirdPartyRender, ThirdPartyRenderRequest},
|
third_party::{ThirdPartyRender, ThirdPartyRenderRequest},
|
||||||
ui::execution::{
|
ui::execution::{
|
||||||
DisplaySeparator, RunAcquireTerminalSnippet, RunImageSnippet, RunSnippetOperation,
|
RunAcquireTerminalSnippet, RunImageSnippet, RunSnippetOperation, SnippetExecutionDisabledOperation,
|
||||||
SnippetExecutionDisabledOperation,
|
disabled::ExecutionType, snippet::DisplaySeparator,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use std::{cell::RefCell, rc::Rc};
|
use std::{cell::RefCell, rc::Rc, sync::Arc};
|
||||||
|
|
||||||
pub(crate) struct SnippetProcessorState<'a> {
|
pub(crate) struct SnippetProcessorState<'a> {
|
||||||
pub(crate) resources: &'a Resources,
|
pub(crate) resources: &'a Resources,
|
||||||
pub(crate) image_registry: &'a ImageRegistry,
|
pub(crate) image_registry: &'a ImageRegistry,
|
||||||
pub(crate) snippet_executor: Rc<SnippetExecutor>,
|
pub(crate) snippet_executor: Arc<SnippetExecutor>,
|
||||||
pub(crate) theme: &'a PresentationTheme,
|
pub(crate) theme: &'a PresentationTheme,
|
||||||
pub(crate) presentation_state: &'a PresentationState,
|
|
||||||
pub(crate) third_party: &'a ThirdPartyRender,
|
pub(crate) third_party: &'a ThirdPartyRender,
|
||||||
pub(crate) highlighter: &'a SnippetHighlighter,
|
pub(crate) highlighter: &'a SnippetHighlighter,
|
||||||
pub(crate) options: &'a PresentationBuilderOptions,
|
pub(crate) options: &'a PresentationBuilderOptions,
|
||||||
pub(crate) slide_number: usize,
|
|
||||||
pub(crate) font_size: u8,
|
pub(crate) font_size: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,13 +41,11 @@ pub(crate) struct SnippetProcessor<'a> {
|
|||||||
mutators: Vec<Box<dyn ChunkMutator>>,
|
mutators: Vec<Box<dyn ChunkMutator>>,
|
||||||
resources: &'a Resources,
|
resources: &'a Resources,
|
||||||
image_registry: &'a ImageRegistry,
|
image_registry: &'a ImageRegistry,
|
||||||
snippet_executor: Rc<SnippetExecutor>,
|
snippet_executor: Arc<SnippetExecutor>,
|
||||||
theme: &'a PresentationTheme,
|
theme: &'a PresentationTheme,
|
||||||
presentation_state: &'a PresentationState,
|
|
||||||
third_party: &'a ThirdPartyRender,
|
third_party: &'a ThirdPartyRender,
|
||||||
highlighter: &'a SnippetHighlighter,
|
highlighter: &'a SnippetHighlighter,
|
||||||
options: &'a PresentationBuilderOptions,
|
options: &'a PresentationBuilderOptions,
|
||||||
slide_number: usize,
|
|
||||||
font_size: u8,
|
font_size: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,11 +56,9 @@ impl<'a> SnippetProcessor<'a> {
|
|||||||
image_registry,
|
image_registry,
|
||||||
snippet_executor,
|
snippet_executor,
|
||||||
theme,
|
theme,
|
||||||
presentation_state,
|
|
||||||
third_party,
|
third_party,
|
||||||
highlighter,
|
highlighter,
|
||||||
options,
|
options,
|
||||||
slide_number,
|
|
||||||
font_size,
|
font_size,
|
||||||
} = state;
|
} = state;
|
||||||
Self {
|
Self {
|
||||||
@ -74,11 +68,9 @@ impl<'a> SnippetProcessor<'a> {
|
|||||||
image_registry,
|
image_registry,
|
||||||
snippet_executor,
|
snippet_executor,
|
||||||
theme,
|
theme,
|
||||||
presentation_state,
|
|
||||||
third_party,
|
third_party,
|
||||||
highlighter,
|
highlighter,
|
||||||
options,
|
options,
|
||||||
slide_number,
|
|
||||||
font_size,
|
font_size,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -128,11 +120,12 @@ impl<'a> SnippetProcessor<'a> {
|
|||||||
match snippet.attributes.execution {
|
match snippet.attributes.execution {
|
||||||
SnippetExec::None => Ok(()),
|
SnippetExec::None => Ok(()),
|
||||||
SnippetExec::Exec | SnippetExec::AcquireTerminal if !execution_allowed => {
|
SnippetExec::Exec | SnippetExec::AcquireTerminal if !execution_allowed => {
|
||||||
let auto_start = match snippet.attributes.representation {
|
let exec_type = match snippet.attributes.representation {
|
||||||
SnippetRepr::Image | SnippetRepr::ExecReplace => true,
|
SnippetRepr::Image => ExecutionType::Image,
|
||||||
SnippetRepr::Render | SnippetRepr::Snippet => false,
|
SnippetRepr::ExecReplace => ExecutionType::ExecReplace,
|
||||||
|
SnippetRepr::Render | SnippetRepr::Snippet => ExecutionType::Execute,
|
||||||
};
|
};
|
||||||
self.push_execution_disabled_operation(auto_start);
|
self.push_execution_disabled_operation(exec_type);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
SnippetExec::Exec => self.push_code_execution(snippet, block_length, ExecutionMode::AlongSnippet),
|
SnippetExec::Exec => self.push_code_execution(snippet, block_length, ExecutionMode::AlongSnippet),
|
||||||
@ -184,7 +177,6 @@ impl<'a> SnippetProcessor<'a> {
|
|||||||
|
|
||||||
fn push_rendered_code(&mut self, code: Snippet, source_position: SourcePosition) -> BuildResult {
|
fn push_rendered_code(&mut self, code: Snippet, source_position: SourcePosition) -> BuildResult {
|
||||||
let Snippet { contents, language, attributes } = code;
|
let Snippet { contents, language, attributes } = code;
|
||||||
let error_holder = self.presentation_state.async_error_holder();
|
|
||||||
let request = match language {
|
let request = match language {
|
||||||
SnippetLanguage::Typst => ThirdPartyRenderRequest::Typst(contents, self.theme.typst.clone()),
|
SnippetLanguage::Typst => ThirdPartyRenderRequest::Typst(contents, self.theme.typst.clone()),
|
||||||
SnippetLanguage::Latex => ThirdPartyRenderRequest::Latex(contents, self.theme.typst.clone()),
|
SnippetLanguage::Latex => ThirdPartyRenderRequest::Latex(contents, self.theme.typst.clone()),
|
||||||
@ -196,8 +188,7 @@ impl<'a> SnippetProcessor<'a> {
|
|||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let operation =
|
let operation = self.third_party.render(request, self.theme, attributes.width)?;
|
||||||
self.third_party.render(request, self.theme, error_holder, self.slide_number, attributes.width)?;
|
|
||||||
self.operations.push(operation);
|
self.operations.push(operation);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -251,14 +242,17 @@ impl<'a> SnippetProcessor<'a> {
|
|||||||
style
|
style
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push_execution_disabled_operation(&mut self, auto_start: bool) {
|
fn push_execution_disabled_operation(&mut self, exec_type: ExecutionType) {
|
||||||
|
let policy = match exec_type {
|
||||||
|
ExecutionType::ExecReplace | ExecutionType::Image => RenderAsyncStartPolicy::Automatic,
|
||||||
|
ExecutionType::Execute => RenderAsyncStartPolicy::OnDemand,
|
||||||
|
};
|
||||||
let operation = SnippetExecutionDisabledOperation::new(
|
let operation = SnippetExecutionDisabledOperation::new(
|
||||||
self.theme.execution_output.status.failure_style,
|
self.theme.execution_output.status.failure_style,
|
||||||
self.theme.code.alignment,
|
self.theme.code.alignment,
|
||||||
|
policy,
|
||||||
|
exec_type,
|
||||||
);
|
);
|
||||||
if auto_start {
|
|
||||||
operation.start_render();
|
|
||||||
}
|
|
||||||
self.operations.push(RenderOperation::RenderAsync(Rc::new(operation)));
|
self.operations.push(RenderOperation::RenderAsync(Rc::new(operation)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -272,8 +266,6 @@ impl<'a> SnippetProcessor<'a> {
|
|||||||
self.image_registry.clone(),
|
self.image_registry.clone(),
|
||||||
self.theme.execution_output.status.clone(),
|
self.theme.execution_output.status.clone(),
|
||||||
);
|
);
|
||||||
operation.start_render();
|
|
||||||
|
|
||||||
let operation = RenderOperation::RenderAsync(Rc::new(operation));
|
let operation = RenderOperation::RenderAsync(Rc::new(operation));
|
||||||
self.operations.push(operation);
|
self.operations.push(operation);
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -310,6 +302,10 @@ impl<'a> SnippetProcessor<'a> {
|
|||||||
if snippet.attributes.no_background {
|
if snippet.attributes.no_background {
|
||||||
execution_output_style.style.colors.background = None;
|
execution_output_style.style.colors.background = None;
|
||||||
}
|
}
|
||||||
|
let policy = match mode {
|
||||||
|
ExecutionMode::AlongSnippet => RenderAsyncStartPolicy::OnDemand,
|
||||||
|
ExecutionMode::ReplaceSnippet => RenderAsyncStartPolicy::Automatic,
|
||||||
|
};
|
||||||
let operation = RunSnippetOperation::new(
|
let operation = RunSnippetOperation::new(
|
||||||
snippet,
|
snippet,
|
||||||
self.snippet_executor.clone(),
|
self.snippet_executor.clone(),
|
||||||
@ -319,10 +315,8 @@ impl<'a> SnippetProcessor<'a> {
|
|||||||
separator,
|
separator,
|
||||||
alignment,
|
alignment,
|
||||||
self.font_size,
|
self.font_size,
|
||||||
|
policy,
|
||||||
);
|
);
|
||||||
if matches!(mode, ExecutionMode::ReplaceSnippet) {
|
|
||||||
operation.start_render();
|
|
||||||
}
|
|
||||||
let operation = RenderOperation::RenderAsync(Rc::new(operation));
|
let operation = RenderOperation::RenderAsync(Rc::new(operation));
|
||||||
self.operations.push(operation);
|
self.operations.push(operation);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -120,7 +120,7 @@ mod test {
|
|||||||
},
|
},
|
||||||
presentation::{Slide, SlideBuilder},
|
presentation::{Slide, SlideBuilder},
|
||||||
render::{
|
render::{
|
||||||
operation::{AsRenderOperations, BlockLine, RenderAsync, RenderAsyncState},
|
operation::{AsRenderOperations, BlockLine, Pollable, RenderAsync, ToggleState},
|
||||||
properties::WindowSize,
|
properties::WindowSize,
|
||||||
},
|
},
|
||||||
theme::{Alignment, Margin},
|
theme::{Alignment, Margin},
|
||||||
@ -138,12 +138,9 @@ mod test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl RenderAsync for Dynamic {
|
impl RenderAsync for Dynamic {
|
||||||
fn start_render(&self) -> bool {
|
fn pollable(&self) -> Box<dyn Pollable> {
|
||||||
false
|
// Use some random one, we don't care
|
||||||
}
|
Box::new(ToggleState::new(Default::default()))
|
||||||
|
|
||||||
fn poll_state(&self) -> RenderAsyncState {
|
|
||||||
RenderAsyncState::Rendered
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,11 +1,7 @@
|
|||||||
use crate::{
|
use crate::{config::OptionsConfig, render::operation::RenderOperation};
|
||||||
config::OptionsConfig,
|
|
||||||
render::operation::{RenderAsyncState, RenderOperation},
|
|
||||||
};
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::{
|
use std::{
|
||||||
cell::RefCell,
|
cell::RefCell,
|
||||||
collections::HashSet,
|
|
||||||
fmt::Debug,
|
fmt::Debug,
|
||||||
ops::Deref,
|
ops::Deref,
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
@ -14,6 +10,7 @@ use std::{
|
|||||||
|
|
||||||
pub(crate) mod builder;
|
pub(crate) mod builder;
|
||||||
pub(crate) mod diff;
|
pub(crate) mod diff;
|
||||||
|
pub(crate) mod poller;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) struct Modals {
|
pub(crate) struct Modals {
|
||||||
@ -143,61 +140,7 @@ impl Presentation {
|
|||||||
self.current_slide().current_chunk_index()
|
self.current_slide().current_chunk_index()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Trigger async render operations in this slide.
|
pub(crate) fn current_slide_mut(&mut self) -> &mut Slide {
|
||||||
pub(crate) fn trigger_slide_async_renders(&mut self) -> bool {
|
|
||||||
let slide = self.current_slide_mut();
|
|
||||||
let mut any_rendered = false;
|
|
||||||
for operation in slide.iter_visible_operations_mut() {
|
|
||||||
if let RenderOperation::RenderAsync(operation) = operation {
|
|
||||||
let is_rendered = operation.start_render();
|
|
||||||
any_rendered = any_rendered || is_rendered;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
any_rendered
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all slides that contain async render operations.
|
|
||||||
pub(crate) fn slides_with_async_renders(&self) -> HashSet<usize> {
|
|
||||||
let mut indexes = HashSet::new();
|
|
||||||
for (index, slide) in self.slides.iter().enumerate() {
|
|
||||||
for operation in slide.iter_operations() {
|
|
||||||
if let RenderOperation::RenderAsync(operation) = operation {
|
|
||||||
if matches!(operation.poll_state(), RenderAsyncState::Rendering { .. }) {
|
|
||||||
indexes.insert(index);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
indexes
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Poll every async render operation in the current slide and check whether they're completed.
|
|
||||||
pub(crate) fn poll_slide_async_renders(&mut self, slide: usize) -> RenderAsyncState {
|
|
||||||
let slide = &mut self.slides[slide];
|
|
||||||
let mut slide_state = RenderAsyncState::Rendered;
|
|
||||||
for operation in slide.iter_operations_mut() {
|
|
||||||
if let RenderOperation::RenderAsync(operation) = operation {
|
|
||||||
let state = operation.poll_state();
|
|
||||||
slide_state = match (&slide_state, &state) {
|
|
||||||
// If one finished rendering and another one still is rendering, claim that we
|
|
||||||
// are still rendering and there's modifications.
|
|
||||||
(RenderAsyncState::JustFinishedRendering, RenderAsyncState::Rendering { .. })
|
|
||||||
| (RenderAsyncState::Rendering { .. }, RenderAsyncState::JustFinishedRendering) => {
|
|
||||||
RenderAsyncState::Rendering { modified: true }
|
|
||||||
}
|
|
||||||
// Render + modified overrides anything, rendering overrides only "rendered".
|
|
||||||
(_, RenderAsyncState::Rendering { modified: true })
|
|
||||||
| (RenderAsyncState::Rendered, RenderAsyncState::Rendering { .. })
|
|
||||||
| (_, RenderAsyncState::JustFinishedRendering) => state,
|
|
||||||
_ => slide_state,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
slide_state
|
|
||||||
}
|
|
||||||
|
|
||||||
fn current_slide_mut(&mut self) -> &mut Slide {
|
|
||||||
let index = self.current_slide_index();
|
let index = self.current_slide_index();
|
||||||
&mut self.slides[index]
|
&mut self.slides[index]
|
||||||
}
|
}
|
||||||
|
118
src/presentation/poller.rs
Normal file
118
src/presentation/poller.rs
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
use crate::render::operation::{Pollable, PollableState};
|
||||||
|
use std::{
|
||||||
|
sync::mpsc::{Receiver, RecvTimeoutError, Sender, channel},
|
||||||
|
thread,
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
const POLL_INTERVAL: Duration = Duration::from_millis(25);
|
||||||
|
|
||||||
|
pub(crate) struct Poller {
|
||||||
|
sender: Sender<PollerCommand>,
|
||||||
|
receiver: Receiver<PollableEffect>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Poller {
|
||||||
|
pub(crate) fn launch() -> Self {
|
||||||
|
let (command_sender, command_receiver) = channel();
|
||||||
|
let (effect_sender, effect_receiver) = channel();
|
||||||
|
let worker = PollerWorker::new(command_receiver, effect_sender);
|
||||||
|
thread::spawn(move || {
|
||||||
|
worker.run();
|
||||||
|
});
|
||||||
|
Self { sender: command_sender, receiver: effect_receiver }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn send(&self, command: PollerCommand) {
|
||||||
|
let _ = self.sender.send(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn next_effect(&mut self) -> Option<PollableEffect> {
|
||||||
|
self.receiver.try_recv().ok()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An effect caused by a pollable.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) enum PollableEffect {
|
||||||
|
/// Refresh the given slide.
|
||||||
|
RefreshSlide(usize),
|
||||||
|
|
||||||
|
/// Display an error for the given slide.
|
||||||
|
DisplayError { slide: usize, error: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A poller command.
|
||||||
|
pub(crate) enum PollerCommand {
|
||||||
|
/// Start polling a pollable that's positioned in the given slide.
|
||||||
|
Poll { pollable: Box<dyn Pollable>, slide: usize },
|
||||||
|
|
||||||
|
/// Reset all pollables.
|
||||||
|
Reset,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PollerWorker {
|
||||||
|
receiver: Receiver<PollerCommand>,
|
||||||
|
sender: Sender<PollableEffect>,
|
||||||
|
pollables: Vec<(Box<dyn Pollable>, usize)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PollerWorker {
|
||||||
|
fn new(receiver: Receiver<PollerCommand>, sender: Sender<PollableEffect>) -> Self {
|
||||||
|
Self { receiver, sender, pollables: Default::default() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(mut self) {
|
||||||
|
loop {
|
||||||
|
match self.receiver.recv_timeout(POLL_INTERVAL) {
|
||||||
|
Ok(command) => self.process_command(command),
|
||||||
|
// TODO don't loop forever.
|
||||||
|
Err(RecvTimeoutError::Timeout) => self.poll(),
|
||||||
|
Err(RecvTimeoutError::Disconnected) => break,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_command(&mut self, command: PollerCommand) {
|
||||||
|
match command {
|
||||||
|
PollerCommand::Poll { mut pollable, slide } => {
|
||||||
|
// Poll and only insert if it's still running.
|
||||||
|
match pollable.poll() {
|
||||||
|
PollableState::Unmodified | PollableState::Modified => {
|
||||||
|
self.pollables.push((pollable, slide));
|
||||||
|
}
|
||||||
|
PollableState::Done => {
|
||||||
|
let _ = self.sender.send(PollableEffect::RefreshSlide(slide));
|
||||||
|
}
|
||||||
|
PollableState::Failed { error } => {
|
||||||
|
let _ = self.sender.send(PollableEffect::DisplayError { slide, error });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
PollerCommand::Reset => self.pollables.clear(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll(&mut self) {
|
||||||
|
let mut removables = Vec::new();
|
||||||
|
for (index, (pollable, slide)) in self.pollables.iter_mut().enumerate() {
|
||||||
|
let slide = *slide;
|
||||||
|
let (effect, remove) = match pollable.poll() {
|
||||||
|
PollableState::Unmodified => (None, false),
|
||||||
|
PollableState::Modified => (Some(PollableEffect::RefreshSlide(slide)), false),
|
||||||
|
PollableState::Done => (Some(PollableEffect::RefreshSlide(slide)), true),
|
||||||
|
PollableState::Failed { error } => (Some(PollableEffect::DisplayError { slide, error }), true),
|
||||||
|
};
|
||||||
|
if let Some(effect) = effect {
|
||||||
|
let _ = self.sender.send(effect);
|
||||||
|
}
|
||||||
|
if remove {
|
||||||
|
removables.push(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Walk back and swap remove to avoid invalidating indexes.
|
||||||
|
for index in removables.iter().rev() {
|
||||||
|
self.pollables.swap_remove(*index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
115
src/presenter.rs
115
src/presenter.rs
@ -10,12 +10,13 @@ use crate::{
|
|||||||
Presentation, Slide,
|
Presentation, Slide,
|
||||||
builder::{BuildError, PresentationBuilder, PresentationBuilderOptions, Themes},
|
builder::{BuildError, PresentationBuilder, PresentationBuilderOptions, Themes},
|
||||||
diff::PresentationDiffer,
|
diff::PresentationDiffer,
|
||||||
|
poller::{PollableEffect, Poller, PollerCommand},
|
||||||
},
|
},
|
||||||
render::{
|
render::{
|
||||||
ErrorSource, RenderError, RenderResult, TerminalDrawer, TerminalDrawerOptions,
|
ErrorSource, RenderError, RenderResult, TerminalDrawer, TerminalDrawerOptions,
|
||||||
ascii_scaler::AsciiScaler,
|
ascii_scaler::AsciiScaler,
|
||||||
engine::{MaxSize, RenderEngine, RenderEngineOptions},
|
engine::{MaxSize, RenderEngine, RenderEngineOptions},
|
||||||
operation::RenderAsyncState,
|
operation::{Pollable, RenderAsyncStartPolicy, RenderOperation},
|
||||||
properties::WindowSize,
|
properties::WindowSize,
|
||||||
validate::OverflowValidator,
|
validate::OverflowValidator,
|
||||||
},
|
},
|
||||||
@ -33,14 +34,12 @@ use crate::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashSet,
|
|
||||||
fmt::Display,
|
fmt::Display,
|
||||||
fs,
|
fs,
|
||||||
io::{self},
|
io::{self},
|
||||||
mem,
|
mem,
|
||||||
ops::Deref,
|
ops::Deref,
|
||||||
path::Path,
|
path::Path,
|
||||||
rc::Rc,
|
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
@ -64,13 +63,13 @@ pub struct Presenter<'a> {
|
|||||||
parser: MarkdownParser<'a>,
|
parser: MarkdownParser<'a>,
|
||||||
resources: Resources,
|
resources: Resources,
|
||||||
third_party: ThirdPartyRender,
|
third_party: ThirdPartyRender,
|
||||||
code_executor: Rc<SnippetExecutor>,
|
code_executor: Arc<SnippetExecutor>,
|
||||||
state: PresenterState,
|
state: PresenterState,
|
||||||
slides_with_pending_async_renders: HashSet<usize>,
|
|
||||||
image_printer: Arc<ImagePrinter>,
|
image_printer: Arc<ImagePrinter>,
|
||||||
themes: Themes,
|
themes: Themes,
|
||||||
options: PresenterOptions,
|
options: PresenterOptions,
|
||||||
speaker_notes_event_publisher: Option<SpeakerNotesEventPublisher>,
|
speaker_notes_event_publisher: Option<SpeakerNotesEventPublisher>,
|
||||||
|
poller: Poller,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Presenter<'a> {
|
impl<'a> Presenter<'a> {
|
||||||
@ -82,7 +81,7 @@ impl<'a> Presenter<'a> {
|
|||||||
parser: MarkdownParser<'a>,
|
parser: MarkdownParser<'a>,
|
||||||
resources: Resources,
|
resources: Resources,
|
||||||
third_party: ThirdPartyRender,
|
third_party: ThirdPartyRender,
|
||||||
code_executor: Rc<SnippetExecutor>,
|
code_executor: Arc<SnippetExecutor>,
|
||||||
themes: Themes,
|
themes: Themes,
|
||||||
image_printer: Arc<ImagePrinter>,
|
image_printer: Arc<ImagePrinter>,
|
||||||
options: PresenterOptions,
|
options: PresenterOptions,
|
||||||
@ -96,11 +95,11 @@ impl<'a> Presenter<'a> {
|
|||||||
third_party,
|
third_party,
|
||||||
code_executor,
|
code_executor,
|
||||||
state: PresenterState::Empty,
|
state: PresenterState::Empty,
|
||||||
slides_with_pending_async_renders: HashSet::new(),
|
|
||||||
image_printer,
|
image_printer,
|
||||||
themes,
|
themes,
|
||||||
options,
|
options,
|
||||||
speaker_notes_event_publisher,
|
speaker_notes_event_publisher,
|
||||||
|
poller: Poller::launch(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,11 +118,10 @@ impl<'a> Presenter<'a> {
|
|||||||
let mut drawer = TerminalDrawer::new(self.image_printer.clone(), drawer_options)?;
|
let mut drawer = TerminalDrawer::new(self.image_printer.clone(), drawer_options)?;
|
||||||
loop {
|
loop {
|
||||||
// Poll async renders once before we draw just in case.
|
// Poll async renders once before we draw just in case.
|
||||||
self.poll_async_renders()?;
|
|
||||||
self.render(&mut drawer)?;
|
self.render(&mut drawer)?;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if self.poll_async_renders()? {
|
if self.process_poller_effects()? {
|
||||||
self.render(&mut drawer)?;
|
self.render(&mut drawer)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,6 +171,36 @@ impl<'a> Presenter<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn process_poller_effects(&mut self) -> Result<bool, PresentationError> {
|
||||||
|
let current_slide = match &self.state {
|
||||||
|
PresenterState::Presenting(presentation)
|
||||||
|
| PresenterState::SlideIndex(presentation)
|
||||||
|
| PresenterState::KeyBindings(presentation)
|
||||||
|
| PresenterState::Failure { presentation, .. } => presentation.current_slide_index(),
|
||||||
|
PresenterState::Empty => usize::MAX,
|
||||||
|
};
|
||||||
|
let mut refreshed = false;
|
||||||
|
let mut needs_render = false;
|
||||||
|
while let Some(effect) = self.poller.next_effect() {
|
||||||
|
match effect {
|
||||||
|
PollableEffect::RefreshSlide(index) => {
|
||||||
|
needs_render = needs_render || index == current_slide;
|
||||||
|
refreshed = true;
|
||||||
|
}
|
||||||
|
PollableEffect::DisplayError { slide, error } => {
|
||||||
|
let presentation = mem::take(&mut self.state).into_presentation();
|
||||||
|
self.state =
|
||||||
|
PresenterState::failure(error, presentation, ErrorSource::Slide(slide + 1), FailureMode::Other);
|
||||||
|
needs_render = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if refreshed {
|
||||||
|
self.try_scale_transition_images()?;
|
||||||
|
}
|
||||||
|
Ok(needs_render)
|
||||||
|
}
|
||||||
|
|
||||||
fn publish_event(&self, event: SpeakerNotesEvent) -> io::Result<()> {
|
fn publish_event(&self, event: SpeakerNotesEvent) -> io::Result<()> {
|
||||||
if let Some(publisher) = &self.speaker_notes_event_publisher {
|
if let Some(publisher) = &self.speaker_notes_event_publisher {
|
||||||
publisher.send(event)?;
|
publisher.send(event)?;
|
||||||
@ -198,31 +226,6 @@ impl<'a> Presenter<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn poll_async_renders(&mut self) -> Result<bool, RenderError> {
|
|
||||||
if matches!(self.state, PresenterState::Failure { .. }) {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
let current_index = self.state.presentation().current_slide_index();
|
|
||||||
self.poll_slide_async_renders(current_index)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn poll_slide_async_renders(&mut self, slide: usize) -> Result<bool, RenderError> {
|
|
||||||
if self.slides_with_pending_async_renders.contains(&slide) {
|
|
||||||
let state = self.state.presentation_mut().poll_slide_async_renders(slide);
|
|
||||||
match state {
|
|
||||||
RenderAsyncState::NotStarted | RenderAsyncState::Rendering { modified: false } => (),
|
|
||||||
RenderAsyncState::Rendering { modified: true } => {
|
|
||||||
return Ok(true);
|
|
||||||
}
|
|
||||||
RenderAsyncState::Rendered | RenderAsyncState::JustFinishedRendering => {
|
|
||||||
self.slides_with_pending_async_renders.remove(&slide);
|
|
||||||
return Ok(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Ok(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render(&mut self, drawer: &mut TerminalDrawer) -> RenderResult {
|
fn render(&mut self, drawer: &mut TerminalDrawer) -> RenderResult {
|
||||||
let result = match &self.state {
|
let result = match &self.state {
|
||||||
PresenterState::Presenting(presentation) => {
|
PresenterState::Presenting(presentation) => {
|
||||||
@ -304,8 +307,11 @@ impl<'a> Presenter<'a> {
|
|||||||
Command::LastSlide => presentation.jump_last_slide(),
|
Command::LastSlide => presentation.jump_last_slide(),
|
||||||
Command::GoToSlide(number) => presentation.go_to_slide(number.saturating_sub(1) as usize),
|
Command::GoToSlide(number) => presentation.go_to_slide(number.saturating_sub(1) as usize),
|
||||||
Command::RenderAsyncOperations => {
|
Command::RenderAsyncOperations => {
|
||||||
if presentation.trigger_slide_async_renders() {
|
let pollables = Self::trigger_slide_async_renders(presentation);
|
||||||
self.slides_with_pending_async_renders.insert(self.state.presentation().current_slide_index());
|
if !pollables.is_empty() {
|
||||||
|
for pollable in pollables {
|
||||||
|
self.poller.send(PollerCommand::Poll { pollable, slide: presentation.current_slide_index() });
|
||||||
|
}
|
||||||
return CommandSideEffect::Redraw;
|
return CommandSideEffect::Redraw;
|
||||||
} else {
|
} else {
|
||||||
return CommandSideEffect::None;
|
return CommandSideEffect::None;
|
||||||
@ -336,7 +342,7 @@ impl<'a> Presenter<'a> {
|
|||||||
if matches!(self.options.mode, PresentMode::Presentation) && !force {
|
if matches!(self.options.mode, PresentMode::Presentation) && !force {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
self.slides_with_pending_async_renders.clear();
|
self.poller.send(PollerCommand::Reset);
|
||||||
self.resources.clear_watches();
|
self.resources.clear_watches();
|
||||||
match self.load_presentation(path) {
|
match self.load_presentation(path) {
|
||||||
Ok(mut presentation) => {
|
Ok(mut presentation) => {
|
||||||
@ -348,7 +354,7 @@ impl<'a> Presenter<'a> {
|
|||||||
presentation.go_to_slide(current.current_slide_index());
|
presentation.go_to_slide(current.current_slide_index());
|
||||||
presentation.jump_chunk(current.current_chunk());
|
presentation.jump_chunk(current.current_chunk());
|
||||||
}
|
}
|
||||||
self.slides_with_pending_async_renders = presentation.slides_with_async_renders().into_iter().collect();
|
self.start_automatic_async_renders(&mut presentation);
|
||||||
self.state = self.validate_overflows(presentation);
|
self.state = self.validate_overflows(presentation);
|
||||||
self.try_scale_transition_images()?;
|
self.try_scale_transition_images()?;
|
||||||
}
|
}
|
||||||
@ -371,6 +377,19 @@ impl<'a> Presenter<'a> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn trigger_slide_async_renders(presentation: &mut Presentation) -> Vec<Box<dyn Pollable>> {
|
||||||
|
let slide = presentation.current_slide_mut();
|
||||||
|
let mut pollables = Vec::new();
|
||||||
|
for operation in slide.iter_visible_operations_mut() {
|
||||||
|
if let RenderOperation::RenderAsync(operation) = operation {
|
||||||
|
if let RenderAsyncStartPolicy::OnDemand = operation.start_policy() {
|
||||||
|
pollables.push(operation.pollable());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pollables
|
||||||
|
}
|
||||||
|
|
||||||
fn is_displaying_other_error(&self) -> bool {
|
fn is_displaying_other_error(&self) -> bool {
|
||||||
matches!(self.state, PresenterState::Failure { mode: FailureMode::Other, .. })
|
matches!(self.state, PresenterState::Failure { mode: FailureMode::Other, .. })
|
||||||
}
|
}
|
||||||
@ -444,7 +463,6 @@ impl<'a> Presenter<'a> {
|
|||||||
let Some(config) = self.options.transition.clone() else {
|
let Some(config) = self.options.transition.clone() else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
self.poll_and_scale_images()?;
|
|
||||||
|
|
||||||
let options = drawer.render_engine_options();
|
let options = drawer.render_engine_options();
|
||||||
let presentation = self.state.presentation_mut();
|
let presentation = self.state.presentation_mut();
|
||||||
@ -461,7 +479,6 @@ impl<'a> Presenter<'a> {
|
|||||||
let Some(config) = self.options.transition.clone() else {
|
let Some(config) = self.options.transition.clone() else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
self.poll_and_scale_images()?;
|
|
||||||
|
|
||||||
let options = drawer.render_engine_options();
|
let options = drawer.render_engine_options();
|
||||||
let presentation = self.state.presentation_mut();
|
let presentation = self.state.presentation_mut();
|
||||||
@ -560,15 +577,17 @@ impl<'a> Presenter<'a> {
|
|||||||
Ok(term.into_contents())
|
Ok(term.into_contents())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn poll_and_scale_images(&mut self) -> RenderResult {
|
fn start_automatic_async_renders(&self, presentation: &mut Presentation) {
|
||||||
let mut needs_scaling = false;
|
for (index, slide) in presentation.iter_slides_mut().enumerate() {
|
||||||
for index in 0..self.state.presentation().iter_slides().count() {
|
for operation in slide.iter_operations_mut() {
|
||||||
needs_scaling = self.poll_slide_async_renders(index)? || needs_scaling;
|
if let RenderOperation::RenderAsync(operation) = operation {
|
||||||
|
if let RenderAsyncStartPolicy::Automatic = operation.start_policy() {
|
||||||
|
let pollable = operation.pollable();
|
||||||
|
self.poller.send(PollerCommand::Poll { pollable, slide: index });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if needs_scaling {
|
|
||||||
self.try_scale_transition_images()?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,11 @@ use crate::{
|
|||||||
terminal::image::Image,
|
terminal::image::Image,
|
||||||
theme::{Alignment, Margin},
|
theme::{Alignment, Margin},
|
||||||
};
|
};
|
||||||
use std::{fmt::Debug, rc::Rc};
|
use std::{
|
||||||
|
fmt::Debug,
|
||||||
|
rc::Rc,
|
||||||
|
sync::{Arc, Mutex},
|
||||||
|
};
|
||||||
|
|
||||||
const DEFAULT_IMAGE_Z_INDEX: i32 = -2;
|
const DEFAULT_IMAGE_Z_INDEX: i32 = -2;
|
||||||
|
|
||||||
@ -111,7 +115,7 @@ impl Default for ImageRenderProperties {
|
|||||||
size: Default::default(),
|
size: Default::default(),
|
||||||
restore_cursor: false,
|
restore_cursor: false,
|
||||||
background_color: None,
|
background_color: None,
|
||||||
position: ImagePosition::Cursor,
|
position: ImagePosition::Center,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -160,24 +164,57 @@ pub(crate) trait AsRenderOperations: Debug + 'static {
|
|||||||
|
|
||||||
/// An operation that can be rendered asynchronously.
|
/// An operation that can be rendered asynchronously.
|
||||||
pub(crate) trait RenderAsync: AsRenderOperations {
|
pub(crate) trait RenderAsync: AsRenderOperations {
|
||||||
/// Start the render for this operation.
|
/// Create a pollable for this render async.
|
||||||
///
|
///
|
||||||
/// Should return true if the invocation triggered the rendering (aka if rendering wasn't
|
/// The pollable will be used to poll this by a separate thread, so all state that will
|
||||||
/// already started before).
|
/// be loaded asynchronously should be shared between this operation and any pollables
|
||||||
fn start_render(&self) -> bool;
|
/// generated from it.
|
||||||
|
fn pollable(&self) -> Box<dyn Pollable>;
|
||||||
|
|
||||||
|
/// Get the start policy for this render.
|
||||||
|
fn start_policy(&self) -> RenderAsyncStartPolicy {
|
||||||
|
RenderAsyncStartPolicy::OnDemand
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The start policy for an async render.
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
pub(crate) enum RenderAsyncStartPolicy {
|
||||||
|
/// Start automatically.
|
||||||
|
Automatic,
|
||||||
|
|
||||||
|
/// Start on demand.
|
||||||
|
OnDemand,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A pollable that can be used to pull and update the state of an operation asynchronously.
|
||||||
|
pub(crate) trait Pollable: Send + 'static {
|
||||||
/// Update the internal state and return the updated state.
|
/// Update the internal state and return the updated state.
|
||||||
fn poll_state(&self) -> RenderAsyncState;
|
fn poll(&mut self) -> PollableState;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The state of a [RenderAsync].
|
/// The state of a [Pollable].
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) enum RenderAsyncState {
|
pub(crate) enum PollableState {
|
||||||
#[default]
|
Unmodified,
|
||||||
NotStarted,
|
Modified,
|
||||||
Rendering {
|
Done,
|
||||||
modified: bool,
|
Failed { error: String },
|
||||||
},
|
}
|
||||||
Rendered,
|
|
||||||
JustFinishedRendering,
|
pub(crate) struct ToggleState {
|
||||||
|
toggled: Arc<Mutex<bool>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToggleState {
|
||||||
|
pub(crate) fn new(toggled: Arc<Mutex<bool>>) -> Self {
|
||||||
|
Self { toggled }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Pollable for ToggleState {
|
||||||
|
fn poll(&mut self) -> PollableState {
|
||||||
|
*self.toggled.lock().unwrap() = true;
|
||||||
|
PollableState::Done
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,10 +5,10 @@ use crate::{
|
|||||||
elements::{Line, Percent, Text},
|
elements::{Line, Percent, Text},
|
||||||
text_style::{Color, TextStyle},
|
text_style::{Color, TextStyle},
|
||||||
},
|
},
|
||||||
presentation::{AsyncPresentationError, AsyncPresentationErrorHolder},
|
|
||||||
render::{
|
render::{
|
||||||
operation::{
|
operation::{
|
||||||
AsRenderOperations, ImageRenderProperties, ImageSize, RenderAsync, RenderAsyncState, RenderOperation,
|
AsRenderOperations, ImageRenderProperties, ImageSize, Pollable, PollableState, RenderAsync,
|
||||||
|
RenderAsyncStartPolicy, RenderOperation,
|
||||||
},
|
},
|
||||||
properties::WindowSize,
|
properties::WindowSize,
|
||||||
},
|
},
|
||||||
@ -53,12 +53,10 @@ impl ThirdPartyRender {
|
|||||||
&self,
|
&self,
|
||||||
request: ThirdPartyRenderRequest,
|
request: ThirdPartyRenderRequest,
|
||||||
theme: &PresentationTheme,
|
theme: &PresentationTheme,
|
||||||
error_holder: AsyncPresentationErrorHolder,
|
|
||||||
slide: usize,
|
|
||||||
width: Option<Percent>,
|
width: Option<Percent>,
|
||||||
) -> Result<RenderOperation, ThirdPartyRenderError> {
|
) -> Result<RenderOperation, ThirdPartyRenderError> {
|
||||||
let result = self.render_pool.render(request);
|
let result = self.render_pool.render(request);
|
||||||
let operation = Rc::new(RenderThirdParty::new(result, theme.default_style.style, error_holder, slide, width));
|
let operation = Rc::new(RenderThirdParty::new(result, theme.default_style.style, width));
|
||||||
Ok(RenderOperation::RenderAsync(operation))
|
Ok(RenderOperation::RenderAsync(operation))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -311,54 +309,32 @@ struct ImageSnippet {
|
|||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) struct RenderThirdParty {
|
pub(crate) struct RenderThirdParty {
|
||||||
contents: Arc<Mutex<Option<Image>>>,
|
contents: Arc<Mutex<Option<Output>>>,
|
||||||
pending_result: Arc<Mutex<RenderResult>>,
|
pending_result: Arc<Mutex<RenderResult>>,
|
||||||
default_style: TextStyle,
|
default_style: TextStyle,
|
||||||
error_holder: AsyncPresentationErrorHolder,
|
|
||||||
slide: usize,
|
|
||||||
width: Option<Percent>,
|
width: Option<Percent>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderThirdParty {
|
impl RenderThirdParty {
|
||||||
fn new(
|
fn new(pending_result: Arc<Mutex<RenderResult>>, default_style: TextStyle, width: Option<Percent>) -> Self {
|
||||||
pending_result: Arc<Mutex<RenderResult>>,
|
Self { contents: Default::default(), pending_result, default_style, width }
|
||||||
default_style: TextStyle,
|
|
||||||
error_holder: AsyncPresentationErrorHolder,
|
|
||||||
slide: usize,
|
|
||||||
width: Option<Percent>,
|
|
||||||
) -> Self {
|
|
||||||
Self { contents: Default::default(), pending_result, default_style, error_holder, slide, width }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderAsync for RenderThirdParty {
|
impl RenderAsync for RenderThirdParty {
|
||||||
fn start_render(&self) -> bool {
|
fn pollable(&self) -> Box<dyn Pollable> {
|
||||||
false
|
Box::new(OperationPollable { contents: self.contents.clone(), pending_result: self.pending_result.clone() })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn poll_state(&self) -> RenderAsyncState {
|
fn start_policy(&self) -> RenderAsyncStartPolicy {
|
||||||
let mut contents = self.contents.lock().unwrap();
|
RenderAsyncStartPolicy::Automatic
|
||||||
if contents.is_some() {
|
|
||||||
return RenderAsyncState::Rendered;
|
|
||||||
}
|
|
||||||
match mem::take(&mut *self.pending_result.lock().unwrap()) {
|
|
||||||
RenderResult::Success(image) => {
|
|
||||||
*contents = Some(image);
|
|
||||||
RenderAsyncState::JustFinishedRendering
|
|
||||||
}
|
|
||||||
RenderResult::Failure(error) => {
|
|
||||||
*self.error_holder.lock().unwrap() = Some(AsyncPresentationError { slide: self.slide, error });
|
|
||||||
RenderAsyncState::JustFinishedRendering
|
|
||||||
}
|
|
||||||
RenderResult::Pending => RenderAsyncState::Rendering { modified: false },
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsRenderOperations for RenderThirdParty {
|
impl AsRenderOperations for RenderThirdParty {
|
||||||
fn as_render_operations(&self, _: &WindowSize) -> Vec<RenderOperation> {
|
fn as_render_operations(&self, _: &WindowSize) -> Vec<RenderOperation> {
|
||||||
match &*self.contents.lock().unwrap() {
|
match &*self.contents.lock().unwrap() {
|
||||||
Some(image) => {
|
Some(Output::Image(image)) => {
|
||||||
let size = match &self.width {
|
let size = match &self.width {
|
||||||
Some(percent) => ImageSize::WidthScaled { ratio: percent.as_ratio() },
|
Some(percent) => ImageSize::WidthScaled { ratio: percent.as_ratio() },
|
||||||
None => Default::default(),
|
None => Default::default(),
|
||||||
@ -371,6 +347,7 @@ impl AsRenderOperations for RenderThirdParty {
|
|||||||
|
|
||||||
vec![RenderOperation::RenderImage(image.clone(), properties)]
|
vec![RenderOperation::RenderImage(image.clone(), properties)]
|
||||||
}
|
}
|
||||||
|
Some(Output::Error) => Vec::new(),
|
||||||
None => {
|
None => {
|
||||||
let text = Line::from(Text::new("Loading...", TextStyle::default().bold()));
|
let text = Line::from(Text::new("Loading...", TextStyle::default().bold()));
|
||||||
vec![RenderOperation::RenderText {
|
vec![RenderOperation::RenderText {
|
||||||
@ -381,3 +358,35 @@ impl AsRenderOperations for RenderThirdParty {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum Output {
|
||||||
|
Image(Image),
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct OperationPollable {
|
||||||
|
contents: Arc<Mutex<Option<Output>>>,
|
||||||
|
pending_result: Arc<Mutex<RenderResult>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Pollable for OperationPollable {
|
||||||
|
fn poll(&mut self) -> PollableState {
|
||||||
|
let mut contents = self.contents.lock().unwrap();
|
||||||
|
if contents.is_some() {
|
||||||
|
return PollableState::Done;
|
||||||
|
}
|
||||||
|
match mem::take(&mut *self.pending_result.lock().unwrap()) {
|
||||||
|
RenderResult::Success(image) => {
|
||||||
|
*contents = Some(Output::Image(image));
|
||||||
|
PollableState::Done
|
||||||
|
}
|
||||||
|
RenderResult::Failure(error) => {
|
||||||
|
*contents = Some(Output::Error);
|
||||||
|
PollableState::Failed { error }
|
||||||
|
}
|
||||||
|
RenderResult::Pending => PollableState::Unmodified,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,495 +0,0 @@
|
|||||||
use super::separator::{RenderSeparator, SeparatorWidth};
|
|
||||||
use crate::{
|
|
||||||
code::{
|
|
||||||
execute::{ExecutionHandle, ExecutionState, ProcessStatus, SnippetExecutor},
|
|
||||||
snippet::Snippet,
|
|
||||||
},
|
|
||||||
markdown::{
|
|
||||||
elements::{Line, Text},
|
|
||||||
text::WeightedLine,
|
|
||||||
text_style::{Colors, TextStyle},
|
|
||||||
},
|
|
||||||
render::{
|
|
||||||
operation::{
|
|
||||||
AsRenderOperations, BlockLine, ImageRenderProperties, RenderAsync, RenderAsyncState, RenderOperation,
|
|
||||||
},
|
|
||||||
properties::WindowSize,
|
|
||||||
},
|
|
||||||
terminal::{
|
|
||||||
ansi::AnsiSplitter,
|
|
||||||
image::{
|
|
||||||
Image,
|
|
||||||
printer::{ImageRegistry, ImageSpec},
|
|
||||||
},
|
|
||||||
should_hide_cursor,
|
|
||||||
},
|
|
||||||
theme::{Alignment, ExecutionOutputBlockStyle, ExecutionStatusBlockStyle, Margin},
|
|
||||||
};
|
|
||||||
use crossterm::{
|
|
||||||
ExecutableCommand, cursor,
|
|
||||||
terminal::{self, disable_raw_mode, enable_raw_mode},
|
|
||||||
};
|
|
||||||
use std::{
|
|
||||||
cell::RefCell,
|
|
||||||
io::{self, BufRead},
|
|
||||||
mem,
|
|
||||||
ops::{Deref, DerefMut},
|
|
||||||
rc::Rc,
|
|
||||||
};
|
|
||||||
|
|
||||||
const MINIMUM_SEPARATOR_WIDTH: u16 = 32;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct RunSnippetOperationInner {
|
|
||||||
handle: Option<ExecutionHandle>,
|
|
||||||
output_lines: Vec<WeightedLine>,
|
|
||||||
state: RenderAsyncState,
|
|
||||||
max_line_length: u16,
|
|
||||||
starting_style: TextStyle,
|
|
||||||
last_length: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub(crate) struct RunSnippetOperation {
|
|
||||||
code: Snippet,
|
|
||||||
executor: Rc<SnippetExecutor>,
|
|
||||||
default_colors: Colors,
|
|
||||||
block_colors: Colors,
|
|
||||||
style: ExecutionStatusBlockStyle,
|
|
||||||
block_length: u16,
|
|
||||||
alignment: Alignment,
|
|
||||||
inner: Rc<RefCell<RunSnippetOperationInner>>,
|
|
||||||
state_description: RefCell<Text>,
|
|
||||||
separator: DisplaySeparator,
|
|
||||||
font_size: u8,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RunSnippetOperation {
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
pub(crate) fn new(
|
|
||||||
code: Snippet,
|
|
||||||
executor: Rc<SnippetExecutor>,
|
|
||||||
default_colors: Colors,
|
|
||||||
style: ExecutionOutputBlockStyle,
|
|
||||||
block_length: u16,
|
|
||||||
separator: DisplaySeparator,
|
|
||||||
alignment: Alignment,
|
|
||||||
font_size: u8,
|
|
||||||
) -> Self {
|
|
||||||
let block_colors = style.style.colors;
|
|
||||||
let status_colors = style.status.clone();
|
|
||||||
let block_length = alignment.adjust_size(block_length);
|
|
||||||
let inner = RunSnippetOperationInner {
|
|
||||||
handle: None,
|
|
||||||
output_lines: Vec::new(),
|
|
||||||
state: RenderAsyncState::default(),
|
|
||||||
max_line_length: 0,
|
|
||||||
starting_style: TextStyle::default().size(font_size),
|
|
||||||
last_length: 0,
|
|
||||||
};
|
|
||||||
Self {
|
|
||||||
code,
|
|
||||||
executor,
|
|
||||||
default_colors,
|
|
||||||
block_colors,
|
|
||||||
style: status_colors,
|
|
||||||
block_length,
|
|
||||||
alignment,
|
|
||||||
inner: Rc::new(RefCell::new(inner)),
|
|
||||||
state_description: Text::new("not started", style.status.not_started_style).into(),
|
|
||||||
separator,
|
|
||||||
font_size,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub(crate) enum DisplaySeparator {
|
|
||||||
On,
|
|
||||||
Off,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsRenderOperations for RunSnippetOperation {
|
|
||||||
fn as_render_operations(&self, _dimensions: &WindowSize) -> Vec<RenderOperation> {
|
|
||||||
let inner = self.inner.borrow();
|
|
||||||
let description = self.state_description.borrow();
|
|
||||||
let mut operations = match self.separator {
|
|
||||||
DisplaySeparator::On => {
|
|
||||||
let heading = Line(vec![" [".into(), description.clone(), "] ".into()]);
|
|
||||||
let separator_width = match &self.alignment {
|
|
||||||
Alignment::Left { .. } | Alignment::Right { .. } => SeparatorWidth::FitToWindow,
|
|
||||||
// We need a minimum here otherwise if the code/block length is too narrow, the separator is
|
|
||||||
// word-wrapped and looks bad.
|
|
||||||
Alignment::Center { .. } => SeparatorWidth::Fixed(self.block_length.max(MINIMUM_SEPARATOR_WIDTH)),
|
|
||||||
};
|
|
||||||
let separator = RenderSeparator::new(heading, separator_width, self.font_size);
|
|
||||||
vec![
|
|
||||||
RenderOperation::RenderLineBreak,
|
|
||||||
RenderOperation::RenderDynamic(Rc::new(separator)),
|
|
||||||
RenderOperation::RenderLineBreak,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
DisplaySeparator::Off => vec![],
|
|
||||||
};
|
|
||||||
if matches!(inner.state, RenderAsyncState::NotStarted) {
|
|
||||||
return operations;
|
|
||||||
}
|
|
||||||
operations.push(RenderOperation::RenderLineBreak);
|
|
||||||
|
|
||||||
if self.block_colors.background.is_some() {
|
|
||||||
operations.push(RenderOperation::SetColors(self.block_colors));
|
|
||||||
}
|
|
||||||
|
|
||||||
let has_margin = match &self.alignment {
|
|
||||||
Alignment::Left { margin } => !margin.is_empty(),
|
|
||||||
Alignment::Right { margin } => !margin.is_empty(),
|
|
||||||
Alignment::Center { minimum_margin, minimum_size } => !minimum_margin.is_empty() || minimum_size != &0,
|
|
||||||
};
|
|
||||||
let block_length =
|
|
||||||
if has_margin { self.block_length.max(inner.max_line_length) } else { inner.max_line_length };
|
|
||||||
for line in &inner.output_lines {
|
|
||||||
operations.push(RenderOperation::RenderBlockLine(BlockLine {
|
|
||||||
prefix: "".into(),
|
|
||||||
right_padding_length: 0,
|
|
||||||
repeat_prefix_on_wrap: false,
|
|
||||||
text: line.clone(),
|
|
||||||
block_length,
|
|
||||||
alignment: self.alignment,
|
|
||||||
block_color: self.block_colors.background,
|
|
||||||
}));
|
|
||||||
operations.push(RenderOperation::RenderLineBreak);
|
|
||||||
}
|
|
||||||
operations.push(RenderOperation::SetColors(self.default_colors));
|
|
||||||
operations
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderAsync for RunSnippetOperation {
|
|
||||||
fn poll_state(&self) -> RenderAsyncState {
|
|
||||||
let mut inner = self.inner.borrow_mut();
|
|
||||||
let last_length = inner.last_length;
|
|
||||||
if let Some(handle) = inner.handle.as_mut() {
|
|
||||||
let mut state = handle.state.lock().unwrap();
|
|
||||||
let ExecutionState { output, status } = &mut *state;
|
|
||||||
*self.state_description.borrow_mut() = match status {
|
|
||||||
ProcessStatus::Running => Text::new("running", self.style.running_style),
|
|
||||||
ProcessStatus::Success => Text::new("finished", self.style.success_style),
|
|
||||||
ProcessStatus::Failure => Text::new("finished with error", self.style.failure_style),
|
|
||||||
};
|
|
||||||
let modified = output.len() != last_length;
|
|
||||||
let is_finished = status.is_finished();
|
|
||||||
let mut lines = Vec::new();
|
|
||||||
for line in output.lines() {
|
|
||||||
let mut line = line.expect("invalid utf8");
|
|
||||||
if line.contains('\t') {
|
|
||||||
line = line.replace('\t', " ");
|
|
||||||
}
|
|
||||||
lines.push(line);
|
|
||||||
}
|
|
||||||
drop(state);
|
|
||||||
|
|
||||||
let mut max_line_length = 0;
|
|
||||||
let (lines, style) = AnsiSplitter::new(inner.starting_style).split_lines(&lines);
|
|
||||||
for line in &lines {
|
|
||||||
let width = u16::try_from(line.width()).unwrap_or(u16::MAX);
|
|
||||||
max_line_length = max_line_length.max(width);
|
|
||||||
}
|
|
||||||
inner.starting_style = style;
|
|
||||||
if is_finished {
|
|
||||||
inner.handle.take();
|
|
||||||
inner.state = RenderAsyncState::JustFinishedRendering;
|
|
||||||
} else {
|
|
||||||
inner.state = RenderAsyncState::Rendering { modified };
|
|
||||||
}
|
|
||||||
inner.output_lines = lines;
|
|
||||||
inner.max_line_length = inner.max_line_length.max(max_line_length);
|
|
||||||
}
|
|
||||||
inner.state.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn start_render(&self) -> bool {
|
|
||||||
let mut inner = self.inner.borrow_mut();
|
|
||||||
if !matches!(inner.state, RenderAsyncState::NotStarted) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
match self.executor.execute_async(&self.code) {
|
|
||||||
Ok(handle) => {
|
|
||||||
inner.handle = Some(handle);
|
|
||||||
inner.state = RenderAsyncState::Rendering { modified: false };
|
|
||||||
true
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
inner.output_lines = vec![WeightedLine::from(e.to_string())];
|
|
||||||
inner.state = RenderAsyncState::Rendered;
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub(crate) struct SnippetExecutionDisabledOperation {
|
|
||||||
style: TextStyle,
|
|
||||||
alignment: Alignment,
|
|
||||||
started: RefCell<bool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SnippetExecutionDisabledOperation {
|
|
||||||
pub(crate) fn new(style: TextStyle, alignment: Alignment) -> Self {
|
|
||||||
Self { style, alignment, started: Default::default() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsRenderOperations for SnippetExecutionDisabledOperation {
|
|
||||||
fn as_render_operations(&self, _: &WindowSize) -> Vec<RenderOperation> {
|
|
||||||
if !*self.started.borrow() {
|
|
||||||
return Vec::new();
|
|
||||||
}
|
|
||||||
vec![
|
|
||||||
RenderOperation::RenderLineBreak,
|
|
||||||
RenderOperation::RenderText {
|
|
||||||
line: vec![Text::new("snippet execution is disabled", self.style)].into(),
|
|
||||||
alignment: self.alignment,
|
|
||||||
},
|
|
||||||
RenderOperation::RenderLineBreak,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderAsync for SnippetExecutionDisabledOperation {
|
|
||||||
fn start_render(&self) -> bool {
|
|
||||||
let was_started = mem::replace(&mut *self.started.borrow_mut(), true);
|
|
||||||
!was_started
|
|
||||||
}
|
|
||||||
|
|
||||||
fn poll_state(&self) -> RenderAsyncState {
|
|
||||||
RenderAsyncState::Rendered
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
|
||||||
enum AcquireTerminalSnippetState {
|
|
||||||
#[default]
|
|
||||||
NotStarted,
|
|
||||||
Success,
|
|
||||||
Failure(Vec<String>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Debug for AcquireTerminalSnippetState {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
Self::NotStarted => write!(f, "NotStarted"),
|
|
||||||
Self::Success => write!(f, "Success"),
|
|
||||||
Self::Failure(_) => write!(f, "Failure"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub(crate) struct RunAcquireTerminalSnippet {
|
|
||||||
snippet: Snippet,
|
|
||||||
block_length: u16,
|
|
||||||
executor: Rc<SnippetExecutor>,
|
|
||||||
colors: ExecutionStatusBlockStyle,
|
|
||||||
state: RefCell<AcquireTerminalSnippetState>,
|
|
||||||
font_size: u8,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RunAcquireTerminalSnippet {
|
|
||||||
pub(crate) fn new(
|
|
||||||
snippet: Snippet,
|
|
||||||
executor: Rc<SnippetExecutor>,
|
|
||||||
colors: ExecutionStatusBlockStyle,
|
|
||||||
block_length: u16,
|
|
||||||
font_size: u8,
|
|
||||||
) -> Self {
|
|
||||||
Self { snippet, block_length, executor, colors, state: Default::default(), font_size }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RunAcquireTerminalSnippet {
|
|
||||||
fn invoke(&self) -> Result<(), String> {
|
|
||||||
let mut stdout = io::stdout();
|
|
||||||
stdout
|
|
||||||
.execute(terminal::LeaveAlternateScreen)
|
|
||||||
.and_then(|_| disable_raw_mode())
|
|
||||||
.map_err(|e| format!("failed to deinit terminal: {e}"))?;
|
|
||||||
|
|
||||||
// save result for later, but first reinit the terminal
|
|
||||||
let result = self.executor.execute_sync(&self.snippet).map_err(|e| format!("failed to run snippet: {e}"));
|
|
||||||
|
|
||||||
stdout
|
|
||||||
.execute(terminal::EnterAlternateScreen)
|
|
||||||
.and_then(|_| enable_raw_mode())
|
|
||||||
.map_err(|e| format!("failed to reinit terminal: {e}"))?;
|
|
||||||
if should_hide_cursor() {
|
|
||||||
stdout.execute(cursor::Hide).map_err(|e| e.to_string())?;
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsRenderOperations for RunAcquireTerminalSnippet {
|
|
||||||
fn as_render_operations(&self, _dimensions: &WindowSize) -> Vec<RenderOperation> {
|
|
||||||
let state = self.state.borrow();
|
|
||||||
let separator_text = match state.deref() {
|
|
||||||
AcquireTerminalSnippetState::NotStarted => Text::new("not started", self.colors.not_started_style),
|
|
||||||
AcquireTerminalSnippetState::Success => Text::new("finished", self.colors.success_style),
|
|
||||||
AcquireTerminalSnippetState::Failure(_) => Text::new("finished with error", self.colors.failure_style),
|
|
||||||
};
|
|
||||||
|
|
||||||
let heading = Line(vec![" [".into(), separator_text, "] ".into()]);
|
|
||||||
let separator_width = SeparatorWidth::Fixed(self.block_length.max(MINIMUM_SEPARATOR_WIDTH));
|
|
||||||
let separator = RenderSeparator::new(heading, separator_width, self.font_size);
|
|
||||||
let mut ops = vec![
|
|
||||||
RenderOperation::RenderLineBreak,
|
|
||||||
RenderOperation::RenderDynamic(Rc::new(separator)),
|
|
||||||
RenderOperation::RenderLineBreak,
|
|
||||||
];
|
|
||||||
if let AcquireTerminalSnippetState::Failure(lines) = state.deref() {
|
|
||||||
ops.push(RenderOperation::RenderLineBreak);
|
|
||||||
for line in lines {
|
|
||||||
ops.extend([
|
|
||||||
RenderOperation::RenderText {
|
|
||||||
line: vec![Text::new(line, self.colors.failure_style)].into(),
|
|
||||||
alignment: Alignment::Left { margin: Margin::Percent(25) },
|
|
||||||
},
|
|
||||||
RenderOperation::RenderLineBreak,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ops
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderAsync for RunAcquireTerminalSnippet {
|
|
||||||
fn start_render(&self) -> bool {
|
|
||||||
if !matches!(*self.state.borrow(), AcquireTerminalSnippetState::NotStarted) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if let Err(e) = self.invoke() {
|
|
||||||
let lines = e.lines().map(ToString::to_string).collect();
|
|
||||||
*self.state.borrow_mut() = AcquireTerminalSnippetState::Failure(lines);
|
|
||||||
} else {
|
|
||||||
*self.state.borrow_mut() = AcquireTerminalSnippetState::Success;
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn poll_state(&self) -> RenderAsyncState {
|
|
||||||
RenderAsyncState::Rendered
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub(crate) struct RunImageSnippet {
|
|
||||||
snippet: Snippet,
|
|
||||||
executor: Rc<SnippetExecutor>,
|
|
||||||
state: RefCell<RunImageSnippetState>,
|
|
||||||
image_registry: ImageRegistry,
|
|
||||||
colors: ExecutionStatusBlockStyle,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RunImageSnippet {
|
|
||||||
pub(crate) fn new(
|
|
||||||
snippet: Snippet,
|
|
||||||
executor: Rc<SnippetExecutor>,
|
|
||||||
image_registry: ImageRegistry,
|
|
||||||
colors: ExecutionStatusBlockStyle,
|
|
||||||
) -> Self {
|
|
||||||
Self { snippet, executor, image_registry, colors, state: Default::default() }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_image(&self, data: &[u8]) -> Result<Image, String> {
|
|
||||||
let image = match image::load_from_memory(data) {
|
|
||||||
Ok(image) => image,
|
|
||||||
Err(e) => {
|
|
||||||
return Err(e.to_string());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
self.image_registry.register(ImageSpec::Generated(image)).map_err(|e| e.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderAsync for RunImageSnippet {
|
|
||||||
fn start_render(&self) -> bool {
|
|
||||||
if !matches!(*self.state.borrow(), RunImageSnippetState::NotStarted) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
let state = match self.executor.execute_async(&self.snippet) {
|
|
||||||
Ok(handle) => RunImageSnippetState::Running(handle),
|
|
||||||
Err(e) => RunImageSnippetState::Failure(e.to_string().lines().map(ToString::to_string).collect()),
|
|
||||||
};
|
|
||||||
*self.state.borrow_mut() = state;
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn poll_state(&self) -> RenderAsyncState {
|
|
||||||
let mut state = self.state.borrow_mut();
|
|
||||||
match state.deref_mut() {
|
|
||||||
RunImageSnippetState::NotStarted => RenderAsyncState::NotStarted,
|
|
||||||
RunImageSnippetState::Running(handle) => {
|
|
||||||
let mut inner = handle.state.lock().unwrap();
|
|
||||||
match inner.status {
|
|
||||||
ProcessStatus::Running => RenderAsyncState::Rendering { modified: false },
|
|
||||||
ProcessStatus::Success => {
|
|
||||||
let data = mem::take(&mut inner.output);
|
|
||||||
drop(inner);
|
|
||||||
|
|
||||||
let image = match self.load_image(&data) {
|
|
||||||
Ok(image) => image,
|
|
||||||
Err(e) => {
|
|
||||||
*state = RunImageSnippetState::Failure(vec![e.to_string()]);
|
|
||||||
return RenderAsyncState::JustFinishedRendering;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
*state = RunImageSnippetState::Success(image);
|
|
||||||
RenderAsyncState::JustFinishedRendering
|
|
||||||
}
|
|
||||||
ProcessStatus::Failure => {
|
|
||||||
let mut lines = Vec::new();
|
|
||||||
for line in inner.output.lines() {
|
|
||||||
lines.push(line.unwrap_or_else(|_| String::new()));
|
|
||||||
}
|
|
||||||
drop(inner);
|
|
||||||
|
|
||||||
*state = RunImageSnippetState::Failure(lines);
|
|
||||||
RenderAsyncState::JustFinishedRendering
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
RunImageSnippetState::Success(_) | RunImageSnippetState::Failure(_) => RenderAsyncState::Rendered,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsRenderOperations for RunImageSnippet {
|
|
||||||
fn as_render_operations(&self, _dimensions: &WindowSize) -> Vec<RenderOperation> {
|
|
||||||
let state = self.state.borrow();
|
|
||||||
match state.deref() {
|
|
||||||
RunImageSnippetState::NotStarted | RunImageSnippetState::Running(_) => vec![],
|
|
||||||
RunImageSnippetState::Success(image) => {
|
|
||||||
vec![RenderOperation::RenderImage(image.clone(), ImageRenderProperties::default())]
|
|
||||||
}
|
|
||||||
RunImageSnippetState::Failure(lines) => {
|
|
||||||
let mut output = Vec::new();
|
|
||||||
for line in lines {
|
|
||||||
output.extend([RenderOperation::RenderText {
|
|
||||||
line: vec![Text::new(line, self.colors.failure_style)].into(),
|
|
||||||
alignment: Alignment::Left { margin: Margin::Percent(25) },
|
|
||||||
}]);
|
|
||||||
}
|
|
||||||
output
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
enum RunImageSnippetState {
|
|
||||||
#[default]
|
|
||||||
NotStarted,
|
|
||||||
Running(ExecutionHandle),
|
|
||||||
Success(Image),
|
|
||||||
Failure(Vec<String>),
|
|
||||||
}
|
|
141
src/ui/execution/acquire_terminal.rs
Normal file
141
src/ui/execution/acquire_terminal.rs
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
use crate::{
|
||||||
|
code::{execute::SnippetExecutor, snippet::Snippet},
|
||||||
|
markdown::elements::{Line, Text},
|
||||||
|
render::{
|
||||||
|
operation::{AsRenderOperations, Pollable, PollableState, RenderAsync, RenderOperation},
|
||||||
|
properties::WindowSize,
|
||||||
|
},
|
||||||
|
terminal::should_hide_cursor,
|
||||||
|
theme::{Alignment, ExecutionStatusBlockStyle, Margin},
|
||||||
|
ui::separator::{RenderSeparator, SeparatorWidth},
|
||||||
|
};
|
||||||
|
use crossterm::{
|
||||||
|
ExecutableCommand, cursor,
|
||||||
|
terminal::{self, disable_raw_mode, enable_raw_mode},
|
||||||
|
};
|
||||||
|
use std::{
|
||||||
|
io::{self},
|
||||||
|
ops::Deref,
|
||||||
|
rc::Rc,
|
||||||
|
sync::{Arc, Mutex},
|
||||||
|
};
|
||||||
|
|
||||||
|
const MINIMUM_SEPARATOR_WIDTH: u16 = 32;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct RunAcquireTerminalSnippet {
|
||||||
|
snippet: Snippet,
|
||||||
|
block_length: u16,
|
||||||
|
executor: Arc<SnippetExecutor>,
|
||||||
|
colors: ExecutionStatusBlockStyle,
|
||||||
|
state: Arc<Mutex<State>>,
|
||||||
|
font_size: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RunAcquireTerminalSnippet {
|
||||||
|
pub(crate) fn new(
|
||||||
|
snippet: Snippet,
|
||||||
|
executor: Arc<SnippetExecutor>,
|
||||||
|
colors: ExecutionStatusBlockStyle,
|
||||||
|
block_length: u16,
|
||||||
|
font_size: u8,
|
||||||
|
) -> Self {
|
||||||
|
Self { snippet, block_length, executor, colors, state: Default::default(), font_size }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn invoke(&self) -> Result<(), String> {
|
||||||
|
let mut stdout = io::stdout();
|
||||||
|
stdout
|
||||||
|
.execute(terminal::LeaveAlternateScreen)
|
||||||
|
.and_then(|_| disable_raw_mode())
|
||||||
|
.map_err(|e| format!("failed to deinit terminal: {e}"))?;
|
||||||
|
|
||||||
|
// save result for later, but first reinit the terminal
|
||||||
|
let result = self.executor.execute_sync(&self.snippet).map_err(|e| format!("failed to run snippet: {e}"));
|
||||||
|
|
||||||
|
stdout
|
||||||
|
.execute(terminal::EnterAlternateScreen)
|
||||||
|
.and_then(|_| enable_raw_mode())
|
||||||
|
.map_err(|e| format!("failed to reinit terminal: {e}"))?;
|
||||||
|
if should_hide_cursor() {
|
||||||
|
stdout.execute(cursor::Hide).map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRenderOperations for RunAcquireTerminalSnippet {
|
||||||
|
fn as_render_operations(&self, _dimensions: &WindowSize) -> Vec<RenderOperation> {
|
||||||
|
let state = self.state.lock().unwrap();
|
||||||
|
let separator_text = match state.deref() {
|
||||||
|
State::NotStarted => Text::new("not started", self.colors.not_started_style),
|
||||||
|
State::Success => Text::new("finished", self.colors.success_style),
|
||||||
|
State::Failure(_) => Text::new("finished with error", self.colors.failure_style),
|
||||||
|
};
|
||||||
|
|
||||||
|
let heading = Line(vec![" [".into(), separator_text, "] ".into()]);
|
||||||
|
let separator_width = SeparatorWidth::Fixed(self.block_length.max(MINIMUM_SEPARATOR_WIDTH));
|
||||||
|
let separator = RenderSeparator::new(heading, separator_width, self.font_size);
|
||||||
|
let mut ops = vec![
|
||||||
|
RenderOperation::RenderLineBreak,
|
||||||
|
RenderOperation::RenderDynamic(Rc::new(separator)),
|
||||||
|
RenderOperation::RenderLineBreak,
|
||||||
|
];
|
||||||
|
if let State::Failure(lines) = state.deref() {
|
||||||
|
ops.push(RenderOperation::RenderLineBreak);
|
||||||
|
for line in lines {
|
||||||
|
ops.extend([
|
||||||
|
RenderOperation::RenderText {
|
||||||
|
line: vec![Text::new(line, self.colors.failure_style)].into(),
|
||||||
|
alignment: Alignment::Left { margin: Margin::Percent(25) },
|
||||||
|
},
|
||||||
|
RenderOperation::RenderLineBreak,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ops
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderAsync for RunAcquireTerminalSnippet {
|
||||||
|
fn pollable(&self) -> Box<dyn Pollable> {
|
||||||
|
// Run within this method because we need to release/acquire the raw terminal in the main
|
||||||
|
// thread.
|
||||||
|
let mut state = self.state.lock().unwrap();
|
||||||
|
if matches!(*state, State::NotStarted) {
|
||||||
|
if let Err(e) = self.invoke() {
|
||||||
|
let lines = e.lines().map(ToString::to_string).collect();
|
||||||
|
*state = State::Failure(lines);
|
||||||
|
} else {
|
||||||
|
*state = State::Success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box::new(OperationPollable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
enum State {
|
||||||
|
#[default]
|
||||||
|
NotStarted,
|
||||||
|
Success,
|
||||||
|
Failure(Vec<String>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for State {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::NotStarted => write!(f, "NotStarted"),
|
||||||
|
Self::Success => write!(f, "Success"),
|
||||||
|
Self::Failure(_) => write!(f, "Failure"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct OperationPollable;
|
||||||
|
|
||||||
|
impl Pollable for OperationPollable {
|
||||||
|
fn poll(&mut self) -> PollableState {
|
||||||
|
PollableState::Done
|
||||||
|
}
|
||||||
|
}
|
64
src/ui/execution/disabled.rs
Normal file
64
src/ui/execution/disabled.rs
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
use crate::{
|
||||||
|
markdown::{elements::Text, text_style::TextStyle},
|
||||||
|
render::{
|
||||||
|
operation::{AsRenderOperations, Pollable, RenderAsync, RenderAsyncStartPolicy, RenderOperation, ToggleState},
|
||||||
|
properties::WindowSize,
|
||||||
|
},
|
||||||
|
theme::Alignment,
|
||||||
|
};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) struct SnippetExecutionDisabledOperation {
|
||||||
|
text: Text,
|
||||||
|
alignment: Alignment,
|
||||||
|
policy: RenderAsyncStartPolicy,
|
||||||
|
toggled: Arc<Mutex<bool>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SnippetExecutionDisabledOperation {
|
||||||
|
pub(crate) fn new(
|
||||||
|
style: TextStyle,
|
||||||
|
alignment: Alignment,
|
||||||
|
policy: RenderAsyncStartPolicy,
|
||||||
|
exec_type: ExecutionType,
|
||||||
|
) -> Self {
|
||||||
|
let (attribute, cli_parameter) = match exec_type {
|
||||||
|
ExecutionType::Execute => ("+exec", "-x"),
|
||||||
|
ExecutionType::ExecReplace => ("+exec_replace", "-X"),
|
||||||
|
ExecutionType::Image => ("+image", "-X"),
|
||||||
|
};
|
||||||
|
let text = Text::new(format!("snippet {attribute} is disabled, run with {cli_parameter} to enable"), style);
|
||||||
|
Self { text, alignment, policy, toggled: Default::default() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRenderOperations for SnippetExecutionDisabledOperation {
|
||||||
|
fn as_render_operations(&self, _: &WindowSize) -> Vec<RenderOperation> {
|
||||||
|
if !*self.toggled.lock().unwrap() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
vec![
|
||||||
|
RenderOperation::RenderLineBreak,
|
||||||
|
RenderOperation::RenderText { line: vec![self.text.clone()].into(), alignment: self.alignment },
|
||||||
|
RenderOperation::RenderLineBreak,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderAsync for SnippetExecutionDisabledOperation {
|
||||||
|
fn pollable(&self) -> Box<dyn Pollable> {
|
||||||
|
Box::new(ToggleState::new(self.toggled.clone()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_policy(&self) -> RenderAsyncStartPolicy {
|
||||||
|
self.policy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) enum ExecutionType {
|
||||||
|
Execute,
|
||||||
|
ExecReplace,
|
||||||
|
Image,
|
||||||
|
}
|
159
src/ui/execution/image.rs
Normal file
159
src/ui/execution/image.rs
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
use crate::{
|
||||||
|
code::{
|
||||||
|
execute::{ExecutionHandle, ProcessStatus, SnippetExecutor},
|
||||||
|
snippet::Snippet,
|
||||||
|
},
|
||||||
|
markdown::elements::Text,
|
||||||
|
render::{
|
||||||
|
operation::{
|
||||||
|
AsRenderOperations, ImageRenderProperties, Pollable, PollableState, RenderAsync, RenderAsyncStartPolicy,
|
||||||
|
RenderOperation,
|
||||||
|
},
|
||||||
|
properties::WindowSize,
|
||||||
|
},
|
||||||
|
terminal::image::{
|
||||||
|
Image,
|
||||||
|
printer::{ImageRegistry, ImageSpec},
|
||||||
|
},
|
||||||
|
theme::{Alignment, ExecutionStatusBlockStyle, Margin},
|
||||||
|
};
|
||||||
|
use std::{
|
||||||
|
io::BufRead,
|
||||||
|
mem,
|
||||||
|
ops::Deref,
|
||||||
|
sync::{Arc, Mutex},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct RunImageSnippet {
|
||||||
|
snippet: Snippet,
|
||||||
|
executor: Arc<SnippetExecutor>,
|
||||||
|
state: Arc<Mutex<State>>,
|
||||||
|
image_registry: ImageRegistry,
|
||||||
|
colors: ExecutionStatusBlockStyle,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RunImageSnippet {
|
||||||
|
pub(crate) fn new(
|
||||||
|
snippet: Snippet,
|
||||||
|
executor: Arc<SnippetExecutor>,
|
||||||
|
image_registry: ImageRegistry,
|
||||||
|
colors: ExecutionStatusBlockStyle,
|
||||||
|
) -> Self {
|
||||||
|
Self { snippet, executor, image_registry, colors, state: Default::default() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderAsync for RunImageSnippet {
|
||||||
|
fn pollable(&self) -> Box<dyn Pollable> {
|
||||||
|
Box::new(OperationPollable {
|
||||||
|
state: self.state.clone(),
|
||||||
|
executor: self.executor.clone(),
|
||||||
|
snippet: self.snippet.clone(),
|
||||||
|
image_registry: self.image_registry.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_policy(&self) -> RenderAsyncStartPolicy {
|
||||||
|
RenderAsyncStartPolicy::Automatic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRenderOperations for RunImageSnippet {
|
||||||
|
fn as_render_operations(&self, _dimensions: &WindowSize) -> Vec<RenderOperation> {
|
||||||
|
let state = self.state.lock().unwrap();
|
||||||
|
match state.deref() {
|
||||||
|
State::NotStarted | State::Running(_) => vec![],
|
||||||
|
State::Success(image) => {
|
||||||
|
vec![RenderOperation::RenderImage(image.clone(), ImageRenderProperties::default())]
|
||||||
|
}
|
||||||
|
State::Failure(lines) => {
|
||||||
|
let mut output = Vec::new();
|
||||||
|
for line in lines {
|
||||||
|
output.extend([RenderOperation::RenderText {
|
||||||
|
line: vec![Text::new(line, self.colors.failure_style)].into(),
|
||||||
|
alignment: Alignment::Left { margin: Margin::Percent(25) },
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
output
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct OperationPollable {
|
||||||
|
state: Arc<Mutex<State>>,
|
||||||
|
executor: Arc<SnippetExecutor>,
|
||||||
|
snippet: Snippet,
|
||||||
|
image_registry: ImageRegistry,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OperationPollable {
|
||||||
|
fn load_image(&self, data: &[u8]) -> Result<Image, String> {
|
||||||
|
let image = match image::load_from_memory(data) {
|
||||||
|
Ok(image) => image,
|
||||||
|
Err(e) => {
|
||||||
|
return Err(e.to_string());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.image_registry.register(ImageSpec::Generated(image)).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Pollable for OperationPollable {
|
||||||
|
fn poll(&mut self) -> PollableState {
|
||||||
|
let mut state = self.state.lock().unwrap();
|
||||||
|
match state.deref() {
|
||||||
|
State::NotStarted => match self.executor.execute_async(&self.snippet) {
|
||||||
|
Ok(handle) => {
|
||||||
|
*state = State::Running(handle);
|
||||||
|
PollableState::Unmodified
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
*state = State::Failure(e.to_string().lines().map(ToString::to_string).collect());
|
||||||
|
PollableState::Done
|
||||||
|
}
|
||||||
|
},
|
||||||
|
State::Running(handle) => {
|
||||||
|
let mut inner = handle.state.lock().unwrap();
|
||||||
|
match inner.status {
|
||||||
|
ProcessStatus::Running => PollableState::Unmodified,
|
||||||
|
ProcessStatus::Success => {
|
||||||
|
let data = mem::take(&mut inner.output);
|
||||||
|
drop(inner);
|
||||||
|
|
||||||
|
match self.load_image(&data) {
|
||||||
|
Ok(image) => {
|
||||||
|
*state = State::Success(image);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
*state = State::Failure(vec![e.to_string()]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
PollableState::Done
|
||||||
|
}
|
||||||
|
ProcessStatus::Failure => {
|
||||||
|
let mut lines = Vec::new();
|
||||||
|
for line in inner.output.lines() {
|
||||||
|
lines.push(line.unwrap_or_else(|_| String::new()));
|
||||||
|
}
|
||||||
|
drop(inner);
|
||||||
|
|
||||||
|
*state = State::Failure(lines);
|
||||||
|
PollableState::Done
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
State::Success(_) | State::Failure(_) => PollableState::Done,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
enum State {
|
||||||
|
#[default]
|
||||||
|
NotStarted,
|
||||||
|
Running(ExecutionHandle),
|
||||||
|
Success(Image),
|
||||||
|
Failure(Vec<String>),
|
||||||
|
}
|
9
src/ui/execution/mod.rs
Normal file
9
src/ui/execution/mod.rs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
pub(crate) mod acquire_terminal;
|
||||||
|
pub(crate) mod disabled;
|
||||||
|
pub(crate) mod image;
|
||||||
|
pub(crate) mod snippet;
|
||||||
|
|
||||||
|
pub(crate) use acquire_terminal::RunAcquireTerminalSnippet;
|
||||||
|
pub(crate) use disabled::SnippetExecutionDisabledOperation;
|
||||||
|
pub(crate) use image::RunImageSnippet;
|
||||||
|
pub(crate) use snippet::RunSnippetOperation;
|
243
src/ui/execution/snippet.rs
Normal file
243
src/ui/execution/snippet.rs
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
use crate::{
|
||||||
|
code::{
|
||||||
|
execute::{ExecutionHandle, ExecutionState, ProcessStatus, SnippetExecutor},
|
||||||
|
snippet::Snippet,
|
||||||
|
},
|
||||||
|
markdown::{
|
||||||
|
elements::{Line, Text},
|
||||||
|
text::WeightedLine,
|
||||||
|
text_style::{Colors, TextStyle},
|
||||||
|
},
|
||||||
|
render::{
|
||||||
|
operation::{
|
||||||
|
AsRenderOperations, BlockLine, Pollable, PollableState, RenderAsync, RenderAsyncStartPolicy,
|
||||||
|
RenderOperation,
|
||||||
|
},
|
||||||
|
properties::WindowSize,
|
||||||
|
},
|
||||||
|
terminal::ansi::AnsiSplitter,
|
||||||
|
theme::{Alignment, ExecutionOutputBlockStyle, ExecutionStatusBlockStyle},
|
||||||
|
ui::separator::{RenderSeparator, SeparatorWidth},
|
||||||
|
};
|
||||||
|
use std::{
|
||||||
|
io::BufRead,
|
||||||
|
rc::Rc,
|
||||||
|
sync::{Arc, Mutex},
|
||||||
|
};
|
||||||
|
|
||||||
|
const MINIMUM_SEPARATOR_WIDTH: u16 = 32;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct Inner {
|
||||||
|
output_lines: Vec<WeightedLine>,
|
||||||
|
max_line_length: u16,
|
||||||
|
process_status: Option<ProcessStatus>,
|
||||||
|
started: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct RunSnippetOperation {
|
||||||
|
code: Snippet,
|
||||||
|
executor: Arc<SnippetExecutor>,
|
||||||
|
default_colors: Colors,
|
||||||
|
block_colors: Colors,
|
||||||
|
style: ExecutionStatusBlockStyle,
|
||||||
|
block_length: u16,
|
||||||
|
alignment: Alignment,
|
||||||
|
inner: Arc<Mutex<Inner>>,
|
||||||
|
separator: DisplaySeparator,
|
||||||
|
font_size: u8,
|
||||||
|
policy: RenderAsyncStartPolicy,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RunSnippetOperation {
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub(crate) fn new(
|
||||||
|
code: Snippet,
|
||||||
|
executor: Arc<SnippetExecutor>,
|
||||||
|
default_colors: Colors,
|
||||||
|
style: ExecutionOutputBlockStyle,
|
||||||
|
block_length: u16,
|
||||||
|
separator: DisplaySeparator,
|
||||||
|
alignment: Alignment,
|
||||||
|
font_size: u8,
|
||||||
|
policy: RenderAsyncStartPolicy,
|
||||||
|
) -> Self {
|
||||||
|
let block_colors = style.style.colors;
|
||||||
|
let status_colors = style.status.clone();
|
||||||
|
let block_length = alignment.adjust_size(block_length);
|
||||||
|
let inner = Inner { output_lines: Vec::new(), max_line_length: 0, process_status: None, started: false };
|
||||||
|
Self {
|
||||||
|
code,
|
||||||
|
executor,
|
||||||
|
default_colors,
|
||||||
|
block_colors,
|
||||||
|
style: status_colors,
|
||||||
|
block_length,
|
||||||
|
alignment,
|
||||||
|
inner: Arc::new(Mutex::new(inner)),
|
||||||
|
separator,
|
||||||
|
font_size,
|
||||||
|
policy,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) enum DisplaySeparator {
|
||||||
|
On,
|
||||||
|
Off,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRenderOperations for RunSnippetOperation {
|
||||||
|
fn as_render_operations(&self, _dimensions: &WindowSize) -> Vec<RenderOperation> {
|
||||||
|
let inner = self.inner.lock().unwrap();
|
||||||
|
let description = match &inner.process_status {
|
||||||
|
Some(ProcessStatus::Running) => Text::new("running", self.style.running_style),
|
||||||
|
Some(ProcessStatus::Success) => Text::new("finished", self.style.success_style),
|
||||||
|
Some(ProcessStatus::Failure) => Text::new("finished with error", self.style.failure_style),
|
||||||
|
None => Text::new("not started", self.style.not_started_style),
|
||||||
|
};
|
||||||
|
let mut operations = match self.separator {
|
||||||
|
DisplaySeparator::On => {
|
||||||
|
let heading = Line(vec![" [".into(), description.clone(), "] ".into()]);
|
||||||
|
let separator_width = match &self.alignment {
|
||||||
|
Alignment::Left { .. } | Alignment::Right { .. } => SeparatorWidth::FitToWindow,
|
||||||
|
// We need a minimum here otherwise if the code/block length is too narrow, the separator is
|
||||||
|
// word-wrapped and looks bad.
|
||||||
|
Alignment::Center { .. } => SeparatorWidth::Fixed(self.block_length.max(MINIMUM_SEPARATOR_WIDTH)),
|
||||||
|
};
|
||||||
|
let separator = RenderSeparator::new(heading, separator_width, self.font_size);
|
||||||
|
vec![
|
||||||
|
RenderOperation::RenderLineBreak,
|
||||||
|
RenderOperation::RenderDynamic(Rc::new(separator)),
|
||||||
|
RenderOperation::RenderLineBreak,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
DisplaySeparator::Off => vec![],
|
||||||
|
};
|
||||||
|
if !inner.started {
|
||||||
|
return operations;
|
||||||
|
}
|
||||||
|
operations.push(RenderOperation::RenderLineBreak);
|
||||||
|
|
||||||
|
if self.block_colors.background.is_some() {
|
||||||
|
operations.push(RenderOperation::SetColors(self.block_colors));
|
||||||
|
}
|
||||||
|
|
||||||
|
let has_margin = match &self.alignment {
|
||||||
|
Alignment::Left { margin } => !margin.is_empty(),
|
||||||
|
Alignment::Right { margin } => !margin.is_empty(),
|
||||||
|
Alignment::Center { minimum_margin, minimum_size } => !minimum_margin.is_empty() || minimum_size != &0,
|
||||||
|
};
|
||||||
|
let block_length =
|
||||||
|
if has_margin { self.block_length.max(inner.max_line_length) } else { inner.max_line_length };
|
||||||
|
for line in &inner.output_lines {
|
||||||
|
operations.push(RenderOperation::RenderBlockLine(BlockLine {
|
||||||
|
prefix: "".into(),
|
||||||
|
right_padding_length: 0,
|
||||||
|
repeat_prefix_on_wrap: false,
|
||||||
|
text: line.clone(),
|
||||||
|
block_length,
|
||||||
|
alignment: self.alignment,
|
||||||
|
block_color: self.block_colors.background,
|
||||||
|
}));
|
||||||
|
operations.push(RenderOperation::RenderLineBreak);
|
||||||
|
}
|
||||||
|
operations.push(RenderOperation::SetColors(self.default_colors));
|
||||||
|
operations
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderAsync for RunSnippetOperation {
|
||||||
|
fn pollable(&self) -> Box<dyn Pollable> {
|
||||||
|
Box::new(OperationPollable {
|
||||||
|
inner: self.inner.clone(),
|
||||||
|
executor: self.executor.clone(),
|
||||||
|
code: self.code.clone(),
|
||||||
|
handle: None,
|
||||||
|
last_length: 0,
|
||||||
|
starting_style: TextStyle::default().size(self.font_size),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_policy(&self) -> RenderAsyncStartPolicy {
|
||||||
|
self.policy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct OperationPollable {
|
||||||
|
inner: Arc<Mutex<Inner>>,
|
||||||
|
executor: Arc<SnippetExecutor>,
|
||||||
|
code: Snippet,
|
||||||
|
handle: Option<ExecutionHandle>,
|
||||||
|
last_length: usize,
|
||||||
|
starting_style: TextStyle,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OperationPollable {
|
||||||
|
fn try_start(&mut self) {
|
||||||
|
let mut inner = self.inner.lock().unwrap();
|
||||||
|
if inner.started {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
inner.started = true;
|
||||||
|
match self.executor.execute_async(&self.code) {
|
||||||
|
Ok(handle) => {
|
||||||
|
self.handle = Some(handle);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
inner.output_lines = vec![WeightedLine::from(e.to_string())];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Pollable for OperationPollable {
|
||||||
|
fn poll(&mut self) -> PollableState {
|
||||||
|
self.try_start();
|
||||||
|
|
||||||
|
// At this point if we don't have a handle it's because we're done.
|
||||||
|
let Some(handle) = self.handle.as_mut() else { return PollableState::Done };
|
||||||
|
|
||||||
|
// Pull data out of the process' output and drop the handle state.
|
||||||
|
let mut state = handle.state.lock().unwrap();
|
||||||
|
let ExecutionState { output, status } = &mut *state;
|
||||||
|
let status = status.clone();
|
||||||
|
|
||||||
|
let modified = output.len() != self.last_length;
|
||||||
|
let mut lines = Vec::new();
|
||||||
|
for line in output.lines() {
|
||||||
|
let mut line = line.expect("invalid utf8");
|
||||||
|
if line.contains('\t') {
|
||||||
|
line = line.replace('\t', " ");
|
||||||
|
}
|
||||||
|
lines.push(line);
|
||||||
|
}
|
||||||
|
drop(state);
|
||||||
|
|
||||||
|
let mut max_line_length = 0;
|
||||||
|
let (lines, style) = AnsiSplitter::new(self.starting_style).split_lines(&lines);
|
||||||
|
for line in &lines {
|
||||||
|
let width = u16::try_from(line.width()).unwrap_or(u16::MAX);
|
||||||
|
max_line_length = max_line_length.max(width);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut inner = self.inner.lock().unwrap();
|
||||||
|
let is_finished = status.is_finished();
|
||||||
|
inner.process_status = Some(status);
|
||||||
|
inner.output_lines = lines;
|
||||||
|
inner.max_line_length = inner.max_line_length.max(max_line_length);
|
||||||
|
if is_finished {
|
||||||
|
self.handle.take();
|
||||||
|
PollableState::Done
|
||||||
|
} else {
|
||||||
|
// Save the style so we continue with it next time
|
||||||
|
self.starting_style = style;
|
||||||
|
match modified {
|
||||||
|
true => PollableState::Modified,
|
||||||
|
false => PollableState::Unmodified,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user