mirror of
https://github.com/mfontanini/presenterm.git
synced 2025-05-05 23:42:59 +00:00
chore: refactor async renders
This commit is contained in:
parent
31f7c6c1e2
commit
5ec3a12d30
@ -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