mirror of
https://github.com/mfontanini/presenterm.git
synced 2025-05-05 15:32:58 +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));
|
||||
}
|
||||
}
|
||||
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)),
|
||||
None => Ok(None),
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ use crate::{
|
||||
terminal::emulator::TerminalEmulator,
|
||||
theme::raw::PresentationTheme,
|
||||
};
|
||||
use std::{io, rc::Rc};
|
||||
use std::{io, sync::Arc};
|
||||
|
||||
const PRESENTATION: &str = r#"
|
||||
# Header 1
|
||||
@ -109,7 +109,7 @@ impl ThemesDemo {
|
||||
theme_options: ThemeOptions { font_size_supported: TerminalEmulator::capabilities().font_size },
|
||||
..Default::default()
|
||||
};
|
||||
let executer = Rc::new(SnippetExecutor::default());
|
||||
let executer = Arc::new(SnippetExecutor::default());
|
||||
let bindings_config = Default::default();
|
||||
let builder = PresentationBuilder::new(
|
||||
theme,
|
||||
|
@ -5,12 +5,13 @@ use crate::{
|
||||
export::pdf::PdfRender,
|
||||
markdown::{parse::ParseError, text_style::Color},
|
||||
presentation::{
|
||||
Presentation, Slide,
|
||||
Presentation,
|
||||
builder::{BuildError, PresentationBuilder, PresentationBuilderOptions, Themes},
|
||||
poller::{Poller, PollerCommand},
|
||||
},
|
||||
render::{
|
||||
RenderError,
|
||||
operation::{AsRenderOperations, RenderAsyncState, RenderOperation},
|
||||
operation::{AsRenderOperations, PollableState, RenderOperation},
|
||||
properties::WindowSize,
|
||||
},
|
||||
theme::{ProcessingThemeError, raw::PresentationTheme},
|
||||
@ -28,8 +29,7 @@ use std::{
|
||||
fs, io,
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
thread::sleep,
|
||||
time::Duration,
|
||||
sync::Arc,
|
||||
};
|
||||
use tempfile::TempDir;
|
||||
|
||||
@ -63,7 +63,7 @@ pub struct Exporter<'a> {
|
||||
default_theme: &'a PresentationTheme,
|
||||
resources: Resources,
|
||||
third_party: ThirdPartyRender,
|
||||
code_executor: Rc<SnippetExecutor>,
|
||||
code_executor: Arc<SnippetExecutor>,
|
||||
themes: Themes,
|
||||
dimensions: WindowSize,
|
||||
options: PresentationBuilderOptions,
|
||||
@ -77,7 +77,7 @@ impl<'a> Exporter<'a> {
|
||||
default_theme: &'a PresentationTheme,
|
||||
resources: Resources,
|
||||
third_party: ThirdPartyRender,
|
||||
code_executor: Rc<SnippetExecutor>,
|
||||
code_executor: Arc<SnippetExecutor>,
|
||||
themes: Themes,
|
||||
mut options: PresentationBuilderOptions,
|
||||
mut dimensions: WindowSize,
|
||||
@ -129,9 +129,7 @@ impl<'a> Exporter<'a> {
|
||||
|
||||
let mut render = PdfRender::new(self.dimensions, output_directory);
|
||||
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(slide);
|
||||
}
|
||||
Self::render_async_images(&mut presentation);
|
||||
for (index, slide) in presentation.into_slides().into_iter().enumerate() {
|
||||
let index = index + 1;
|
||||
Self::log(&format!("processing slide {index}..."))?;
|
||||
@ -154,24 +152,33 @@ impl<'a> Exporter<'a> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render_async_images(slide: &mut Slide) {
|
||||
for op in slide.iter_operations_mut() {
|
||||
if let RenderOperation::RenderAsync(inner) = op {
|
||||
loop {
|
||||
match inner.poll_state() {
|
||||
RenderAsyncState::Rendering { .. } => {
|
||||
sleep(Duration::from_millis(200));
|
||||
continue;
|
||||
}
|
||||
RenderAsyncState::Rendered | RenderAsyncState::JustFinishedRendering => break,
|
||||
RenderAsyncState::NotStarted => inner.start_render(),
|
||||
};
|
||||
fn render_async_images(presentation: &mut Presentation) {
|
||||
let poller = Poller::launch();
|
||||
let mut pollables = Vec::new();
|
||||
for (index, slide) in presentation.iter_slides().enumerate() {
|
||||
for op in slide.iter_operations() {
|
||||
if let RenderOperation::RenderAsync(inner) = op {
|
||||
// Send a pollable to the poller and keep one for ourselves.
|
||||
poller.send(PollerCommand::Poll { pollable: inner.pollable(), slide: index });
|
||||
pollables.push(inner.pollable())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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},
|
||||
io,
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
use terminal::emulator::TerminalEmulator;
|
||||
@ -192,7 +191,7 @@ impl Customizations {
|
||||
|
||||
struct CoreComponents {
|
||||
third_party: ThirdPartyRender,
|
||||
code_executor: Rc<SnippetExecutor>,
|
||||
code_executor: Arc<SnippetExecutor>,
|
||||
resources: Resources,
|
||||
printer: Arc<ImagePrinter>,
|
||||
builder_options: PresentationBuilderOptions,
|
||||
@ -242,7 +241,7 @@ impl CoreComponents {
|
||||
threads: config.snippet.render.threads,
|
||||
};
|
||||
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 {
|
||||
third_party,
|
||||
code_executor,
|
||||
|
@ -40,7 +40,7 @@ use comrak::{Arena, nodes::AlertType};
|
||||
use image::DynamicImage;
|
||||
use serde::Deserialize;
|
||||
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;
|
||||
|
||||
mod snippet;
|
||||
@ -125,7 +125,7 @@ pub(crate) struct PresentationBuilder<'a> {
|
||||
chunk_mutators: Vec<Box<dyn ChunkMutator>>,
|
||||
slide_builders: Vec<SlideBuilder>,
|
||||
highlighter: SnippetHighlighter,
|
||||
code_executor: Rc<SnippetExecutor>,
|
||||
code_executor: Arc<SnippetExecutor>,
|
||||
theme: PresentationTheme,
|
||||
default_raw_theme: &'a raw::PresentationTheme,
|
||||
resources: Resources,
|
||||
@ -148,7 +148,7 @@ impl<'a> PresentationBuilder<'a> {
|
||||
default_raw_theme: &'a raw::PresentationTheme,
|
||||
resources: Resources,
|
||||
third_party: &'a mut ThirdPartyRender,
|
||||
code_executor: Rc<SnippetExecutor>,
|
||||
code_executor: Arc<SnippetExecutor>,
|
||||
themes: &'a Themes,
|
||||
image_registry: ImageRegistry,
|
||||
bindings_config: KeyBindingsConfig,
|
||||
@ -934,11 +934,9 @@ impl<'a> PresentationBuilder<'a> {
|
||||
image_registry: &self.image_registry,
|
||||
snippet_executor: self.code_executor.clone(),
|
||||
theme: &self.theme,
|
||||
presentation_state: &self.presentation_state,
|
||||
third_party: self.third_party,
|
||||
highlighter: &self.highlighter,
|
||||
options: &self.options,
|
||||
slide_number: self.slide_builders.len() + 1,
|
||||
font_size: self.slide_font_size(),
|
||||
};
|
||||
let processor = SnippetProcessor::new(state);
|
||||
@ -1174,6 +1172,7 @@ pub enum BuildError {
|
||||
InvalidFooter(#[from] InvalidFooterTemplateError),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ExecutionMode {
|
||||
AlongSnippet,
|
||||
ReplaceSnippet,
|
||||
@ -1371,7 +1370,7 @@ mod test {
|
||||
let tmp_dir = std::env::temp_dir();
|
||||
let resources = Resources::new(&tmp_dir, &tmp_dir, Default::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 bindings = KeyBindingsConfig::default();
|
||||
let builder = PresentationBuilder::new(
|
||||
|
@ -10,31 +10,29 @@ use crate::{
|
||||
},
|
||||
},
|
||||
markdown::elements::SourcePosition,
|
||||
presentation::{ChunkMutator, PresentationState},
|
||||
presentation::ChunkMutator,
|
||||
render::{
|
||||
operation::{AsRenderOperations, RenderAsync, RenderOperation},
|
||||
operation::{AsRenderOperations, RenderAsyncStartPolicy, RenderOperation},
|
||||
properties::WindowSize,
|
||||
},
|
||||
resource::Resources,
|
||||
theme::{CodeBlockStyle, PresentationTheme},
|
||||
third_party::{ThirdPartyRender, ThirdPartyRenderRequest},
|
||||
ui::execution::{
|
||||
DisplaySeparator, RunAcquireTerminalSnippet, RunImageSnippet, RunSnippetOperation,
|
||||
SnippetExecutionDisabledOperation,
|
||||
RunAcquireTerminalSnippet, RunImageSnippet, RunSnippetOperation, 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) resources: &'a Resources,
|
||||
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) presentation_state: &'a PresentationState,
|
||||
pub(crate) third_party: &'a ThirdPartyRender,
|
||||
pub(crate) highlighter: &'a SnippetHighlighter,
|
||||
pub(crate) options: &'a PresentationBuilderOptions,
|
||||
pub(crate) slide_number: usize,
|
||||
pub(crate) font_size: u8,
|
||||
}
|
||||
|
||||
@ -43,13 +41,11 @@ pub(crate) struct SnippetProcessor<'a> {
|
||||
mutators: Vec<Box<dyn ChunkMutator>>,
|
||||
resources: &'a Resources,
|
||||
image_registry: &'a ImageRegistry,
|
||||
snippet_executor: Rc<SnippetExecutor>,
|
||||
snippet_executor: Arc<SnippetExecutor>,
|
||||
theme: &'a PresentationTheme,
|
||||
presentation_state: &'a PresentationState,
|
||||
third_party: &'a ThirdPartyRender,
|
||||
highlighter: &'a SnippetHighlighter,
|
||||
options: &'a PresentationBuilderOptions,
|
||||
slide_number: usize,
|
||||
font_size: u8,
|
||||
}
|
||||
|
||||
@ -60,11 +56,9 @@ impl<'a> SnippetProcessor<'a> {
|
||||
image_registry,
|
||||
snippet_executor,
|
||||
theme,
|
||||
presentation_state,
|
||||
third_party,
|
||||
highlighter,
|
||||
options,
|
||||
slide_number,
|
||||
font_size,
|
||||
} = state;
|
||||
Self {
|
||||
@ -74,11 +68,9 @@ impl<'a> SnippetProcessor<'a> {
|
||||
image_registry,
|
||||
snippet_executor,
|
||||
theme,
|
||||
presentation_state,
|
||||
third_party,
|
||||
highlighter,
|
||||
options,
|
||||
slide_number,
|
||||
font_size,
|
||||
}
|
||||
}
|
||||
@ -128,11 +120,12 @@ impl<'a> SnippetProcessor<'a> {
|
||||
match snippet.attributes.execution {
|
||||
SnippetExec::None => Ok(()),
|
||||
SnippetExec::Exec | SnippetExec::AcquireTerminal if !execution_allowed => {
|
||||
let auto_start = match snippet.attributes.representation {
|
||||
SnippetRepr::Image | SnippetRepr::ExecReplace => true,
|
||||
SnippetRepr::Render | SnippetRepr::Snippet => false,
|
||||
let exec_type = match snippet.attributes.representation {
|
||||
SnippetRepr::Image => ExecutionType::Image,
|
||||
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(())
|
||||
}
|
||||
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 {
|
||||
let Snippet { contents, language, attributes } = code;
|
||||
let error_holder = self.presentation_state.async_error_holder();
|
||||
let request = match language {
|
||||
SnippetLanguage::Typst => ThirdPartyRenderRequest::Typst(contents, self.theme.typst.clone()),
|
||||
SnippetLanguage::Latex => ThirdPartyRenderRequest::Latex(contents, self.theme.typst.clone()),
|
||||
@ -196,8 +188,7 @@ impl<'a> SnippetProcessor<'a> {
|
||||
})?;
|
||||
}
|
||||
};
|
||||
let operation =
|
||||
self.third_party.render(request, self.theme, error_holder, self.slide_number, attributes.width)?;
|
||||
let operation = self.third_party.render(request, self.theme, attributes.width)?;
|
||||
self.operations.push(operation);
|
||||
Ok(())
|
||||
}
|
||||
@ -251,14 +242,17 @@ impl<'a> SnippetProcessor<'a> {
|
||||
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(
|
||||
self.theme.execution_output.status.failure_style,
|
||||
self.theme.code.alignment,
|
||||
policy,
|
||||
exec_type,
|
||||
);
|
||||
if auto_start {
|
||||
operation.start_render();
|
||||
}
|
||||
self.operations.push(RenderOperation::RenderAsync(Rc::new(operation)));
|
||||
}
|
||||
|
||||
@ -272,8 +266,6 @@ impl<'a> SnippetProcessor<'a> {
|
||||
self.image_registry.clone(),
|
||||
self.theme.execution_output.status.clone(),
|
||||
);
|
||||
operation.start_render();
|
||||
|
||||
let operation = RenderOperation::RenderAsync(Rc::new(operation));
|
||||
self.operations.push(operation);
|
||||
Ok(())
|
||||
@ -310,6 +302,10 @@ impl<'a> SnippetProcessor<'a> {
|
||||
if snippet.attributes.no_background {
|
||||
execution_output_style.style.colors.background = None;
|
||||
}
|
||||
let policy = match mode {
|
||||
ExecutionMode::AlongSnippet => RenderAsyncStartPolicy::OnDemand,
|
||||
ExecutionMode::ReplaceSnippet => RenderAsyncStartPolicy::Automatic,
|
||||
};
|
||||
let operation = RunSnippetOperation::new(
|
||||
snippet,
|
||||
self.snippet_executor.clone(),
|
||||
@ -319,10 +315,8 @@ impl<'a> SnippetProcessor<'a> {
|
||||
separator,
|
||||
alignment,
|
||||
self.font_size,
|
||||
policy,
|
||||
);
|
||||
if matches!(mode, ExecutionMode::ReplaceSnippet) {
|
||||
operation.start_render();
|
||||
}
|
||||
let operation = RenderOperation::RenderAsync(Rc::new(operation));
|
||||
self.operations.push(operation);
|
||||
Ok(())
|
||||
|
@ -120,7 +120,7 @@ mod test {
|
||||
},
|
||||
presentation::{Slide, SlideBuilder},
|
||||
render::{
|
||||
operation::{AsRenderOperations, BlockLine, RenderAsync, RenderAsyncState},
|
||||
operation::{AsRenderOperations, BlockLine, Pollable, RenderAsync, ToggleState},
|
||||
properties::WindowSize,
|
||||
},
|
||||
theme::{Alignment, Margin},
|
||||
@ -138,12 +138,9 @@ mod test {
|
||||
}
|
||||
|
||||
impl RenderAsync for Dynamic {
|
||||
fn start_render(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn poll_state(&self) -> RenderAsyncState {
|
||||
RenderAsyncState::Rendered
|
||||
fn pollable(&self) -> Box<dyn Pollable> {
|
||||
// Use some random one, we don't care
|
||||
Box::new(ToggleState::new(Default::default()))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,7 @@
|
||||
use crate::{
|
||||
config::OptionsConfig,
|
||||
render::operation::{RenderAsyncState, RenderOperation},
|
||||
};
|
||||
use crate::{config::OptionsConfig, render::operation::RenderOperation};
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
collections::HashSet,
|
||||
fmt::Debug,
|
||||
ops::Deref,
|
||||
rc::Rc,
|
||||
@ -14,6 +10,7 @@ use std::{
|
||||
|
||||
pub(crate) mod builder;
|
||||
pub(crate) mod diff;
|
||||
pub(crate) mod poller;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Modals {
|
||||
@ -143,61 +140,7 @@ impl Presentation {
|
||||
self.current_slide().current_chunk_index()
|
||||
}
|
||||
|
||||
/// Trigger async render operations in this 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 {
|
||||
pub(crate) fn current_slide_mut(&mut self) -> &mut Slide {
|
||||
let index = self.current_slide_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,
|
||||
builder::{BuildError, PresentationBuilder, PresentationBuilderOptions, Themes},
|
||||
diff::PresentationDiffer,
|
||||
poller::{PollableEffect, Poller, PollerCommand},
|
||||
},
|
||||
render::{
|
||||
ErrorSource, RenderError, RenderResult, TerminalDrawer, TerminalDrawerOptions,
|
||||
ascii_scaler::AsciiScaler,
|
||||
engine::{MaxSize, RenderEngine, RenderEngineOptions},
|
||||
operation::RenderAsyncState,
|
||||
operation::{Pollable, RenderAsyncStartPolicy, RenderOperation},
|
||||
properties::WindowSize,
|
||||
validate::OverflowValidator,
|
||||
},
|
||||
@ -33,14 +34,12 @@ use crate::{
|
||||
},
|
||||
};
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fmt::Display,
|
||||
fs,
|
||||
io::{self},
|
||||
mem,
|
||||
ops::Deref,
|
||||
path::Path,
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
@ -64,13 +63,13 @@ pub struct Presenter<'a> {
|
||||
parser: MarkdownParser<'a>,
|
||||
resources: Resources,
|
||||
third_party: ThirdPartyRender,
|
||||
code_executor: Rc<SnippetExecutor>,
|
||||
code_executor: Arc<SnippetExecutor>,
|
||||
state: PresenterState,
|
||||
slides_with_pending_async_renders: HashSet<usize>,
|
||||
image_printer: Arc<ImagePrinter>,
|
||||
themes: Themes,
|
||||
options: PresenterOptions,
|
||||
speaker_notes_event_publisher: Option<SpeakerNotesEventPublisher>,
|
||||
poller: Poller,
|
||||
}
|
||||
|
||||
impl<'a> Presenter<'a> {
|
||||
@ -82,7 +81,7 @@ impl<'a> Presenter<'a> {
|
||||
parser: MarkdownParser<'a>,
|
||||
resources: Resources,
|
||||
third_party: ThirdPartyRender,
|
||||
code_executor: Rc<SnippetExecutor>,
|
||||
code_executor: Arc<SnippetExecutor>,
|
||||
themes: Themes,
|
||||
image_printer: Arc<ImagePrinter>,
|
||||
options: PresenterOptions,
|
||||
@ -96,11 +95,11 @@ impl<'a> Presenter<'a> {
|
||||
third_party,
|
||||
code_executor,
|
||||
state: PresenterState::Empty,
|
||||
slides_with_pending_async_renders: HashSet::new(),
|
||||
image_printer,
|
||||
themes,
|
||||
options,
|
||||
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)?;
|
||||
loop {
|
||||
// Poll async renders once before we draw just in case.
|
||||
self.poll_async_renders()?;
|
||||
self.render(&mut drawer)?;
|
||||
|
||||
loop {
|
||||
if self.poll_async_renders()? {
|
||||
if self.process_poller_effects()? {
|
||||
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<()> {
|
||||
if let Some(publisher) = &self.speaker_notes_event_publisher {
|
||||
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 {
|
||||
let result = match &self.state {
|
||||
PresenterState::Presenting(presentation) => {
|
||||
@ -304,8 +307,11 @@ impl<'a> Presenter<'a> {
|
||||
Command::LastSlide => presentation.jump_last_slide(),
|
||||
Command::GoToSlide(number) => presentation.go_to_slide(number.saturating_sub(1) as usize),
|
||||
Command::RenderAsyncOperations => {
|
||||
if presentation.trigger_slide_async_renders() {
|
||||
self.slides_with_pending_async_renders.insert(self.state.presentation().current_slide_index());
|
||||
let pollables = Self::trigger_slide_async_renders(presentation);
|
||||
if !pollables.is_empty() {
|
||||
for pollable in pollables {
|
||||
self.poller.send(PollerCommand::Poll { pollable, slide: presentation.current_slide_index() });
|
||||
}
|
||||
return CommandSideEffect::Redraw;
|
||||
} else {
|
||||
return CommandSideEffect::None;
|
||||
@ -336,7 +342,7 @@ impl<'a> Presenter<'a> {
|
||||
if matches!(self.options.mode, PresentMode::Presentation) && !force {
|
||||
return Ok(());
|
||||
}
|
||||
self.slides_with_pending_async_renders.clear();
|
||||
self.poller.send(PollerCommand::Reset);
|
||||
self.resources.clear_watches();
|
||||
match self.load_presentation(path) {
|
||||
Ok(mut presentation) => {
|
||||
@ -348,7 +354,7 @@ impl<'a> Presenter<'a> {
|
||||
presentation.go_to_slide(current.current_slide_index());
|
||||
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.try_scale_transition_images()?;
|
||||
}
|
||||
@ -371,6 +377,19 @@ impl<'a> Presenter<'a> {
|
||||
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 {
|
||||
matches!(self.state, PresenterState::Failure { mode: FailureMode::Other, .. })
|
||||
}
|
||||
@ -444,7 +463,6 @@ impl<'a> Presenter<'a> {
|
||||
let Some(config) = self.options.transition.clone() else {
|
||||
return Ok(());
|
||||
};
|
||||
self.poll_and_scale_images()?;
|
||||
|
||||
let options = drawer.render_engine_options();
|
||||
let presentation = self.state.presentation_mut();
|
||||
@ -461,7 +479,6 @@ impl<'a> Presenter<'a> {
|
||||
let Some(config) = self.options.transition.clone() else {
|
||||
return Ok(());
|
||||
};
|
||||
self.poll_and_scale_images()?;
|
||||
|
||||
let options = drawer.render_engine_options();
|
||||
let presentation = self.state.presentation_mut();
|
||||
@ -560,15 +577,17 @@ impl<'a> Presenter<'a> {
|
||||
Ok(term.into_contents())
|
||||
}
|
||||
|
||||
fn poll_and_scale_images(&mut self) -> RenderResult {
|
||||
let mut needs_scaling = false;
|
||||
for index in 0..self.state.presentation().iter_slides().count() {
|
||||
needs_scaling = self.poll_slide_async_renders(index)? || needs_scaling;
|
||||
fn start_automatic_async_renders(&self, presentation: &mut Presentation) {
|
||||
for (index, slide) in presentation.iter_slides_mut().enumerate() {
|
||||
for operation in slide.iter_operations_mut() {
|
||||
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,
|
||||
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;
|
||||
|
||||
@ -111,7 +115,7 @@ impl Default for ImageRenderProperties {
|
||||
size: Default::default(),
|
||||
restore_cursor: false,
|
||||
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.
|
||||
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
|
||||
/// already started before).
|
||||
fn start_render(&self) -> bool;
|
||||
/// The pollable will be used to poll this by a separate thread, so all state that will
|
||||
/// be loaded asynchronously should be shared between this operation and any pollables
|
||||
/// 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.
|
||||
fn poll_state(&self) -> RenderAsyncState;
|
||||
fn poll(&mut self) -> PollableState;
|
||||
}
|
||||
|
||||
/// The state of a [RenderAsync].
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub(crate) enum RenderAsyncState {
|
||||
#[default]
|
||||
NotStarted,
|
||||
Rendering {
|
||||
modified: bool,
|
||||
},
|
||||
Rendered,
|
||||
JustFinishedRendering,
|
||||
/// The state of a [Pollable].
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum PollableState {
|
||||
Unmodified,
|
||||
Modified,
|
||||
Done,
|
||||
Failed { error: String },
|
||||
}
|
||||
|
||||
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},
|
||||
text_style::{Color, TextStyle},
|
||||
},
|
||||
presentation::{AsyncPresentationError, AsyncPresentationErrorHolder},
|
||||
render::{
|
||||
operation::{
|
||||
AsRenderOperations, ImageRenderProperties, ImageSize, RenderAsync, RenderAsyncState, RenderOperation,
|
||||
AsRenderOperations, ImageRenderProperties, ImageSize, Pollable, PollableState, RenderAsync,
|
||||
RenderAsyncStartPolicy, RenderOperation,
|
||||
},
|
||||
properties::WindowSize,
|
||||
},
|
||||
@ -53,12 +53,10 @@ impl ThirdPartyRender {
|
||||
&self,
|
||||
request: ThirdPartyRenderRequest,
|
||||
theme: &PresentationTheme,
|
||||
error_holder: AsyncPresentationErrorHolder,
|
||||
slide: usize,
|
||||
width: Option<Percent>,
|
||||
) -> Result<RenderOperation, ThirdPartyRenderError> {
|
||||
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))
|
||||
}
|
||||
}
|
||||
@ -311,54 +309,32 @@ struct ImageSnippet {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct RenderThirdParty {
|
||||
contents: Arc<Mutex<Option<Image>>>,
|
||||
contents: Arc<Mutex<Option<Output>>>,
|
||||
pending_result: Arc<Mutex<RenderResult>>,
|
||||
default_style: TextStyle,
|
||||
error_holder: AsyncPresentationErrorHolder,
|
||||
slide: usize,
|
||||
width: Option<Percent>,
|
||||
}
|
||||
|
||||
impl RenderThirdParty {
|
||||
fn new(
|
||||
pending_result: Arc<Mutex<RenderResult>>,
|
||||
default_style: TextStyle,
|
||||
error_holder: AsyncPresentationErrorHolder,
|
||||
slide: usize,
|
||||
width: Option<Percent>,
|
||||
) -> Self {
|
||||
Self { contents: Default::default(), pending_result, default_style, error_holder, slide, width }
|
||||
fn new(pending_result: Arc<Mutex<RenderResult>>, default_style: TextStyle, width: Option<Percent>) -> Self {
|
||||
Self { contents: Default::default(), pending_result, default_style, width }
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderAsync for RenderThirdParty {
|
||||
fn start_render(&self) -> bool {
|
||||
false
|
||||
fn pollable(&self) -> Box<dyn Pollable> {
|
||||
Box::new(OperationPollable { contents: self.contents.clone(), pending_result: self.pending_result.clone() })
|
||||
}
|
||||
|
||||
fn poll_state(&self) -> RenderAsyncState {
|
||||
let mut contents = self.contents.lock().unwrap();
|
||||
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 },
|
||||
}
|
||||
fn start_policy(&self) -> RenderAsyncStartPolicy {
|
||||
RenderAsyncStartPolicy::Automatic
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRenderOperations for RenderThirdParty {
|
||||
fn as_render_operations(&self, _: &WindowSize) -> Vec<RenderOperation> {
|
||||
match &*self.contents.lock().unwrap() {
|
||||
Some(image) => {
|
||||
Some(Output::Image(image)) => {
|
||||
let size = match &self.width {
|
||||
Some(percent) => ImageSize::WidthScaled { ratio: percent.as_ratio() },
|
||||
None => Default::default(),
|
||||
@ -371,6 +347,7 @@ impl AsRenderOperations for RenderThirdParty {
|
||||
|
||||
vec![RenderOperation::RenderImage(image.clone(), properties)]
|
||||
}
|
||||
Some(Output::Error) => Vec::new(),
|
||||
None => {
|
||||
let text = Line::from(Text::new("Loading...", TextStyle::default().bold()));
|
||||
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