Skip to content

Commit

Permalink
Render on demand widgets on user command
Browse files Browse the repository at this point in the history
  • Loading branch information
mfontanini committed Oct 21, 2023
1 parent 973e988 commit 8b2aadf
Show file tree
Hide file tree
Showing 12 changed files with 305 additions and 31 deletions.
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ crossterm = { version = "0.27", features = ["serde"] }
hex = "0.4"
image = "0.24"
merge-struct = "0.1.0"
itertools = "0.11"
once_cell = "1.18"
serde = { version = "1.0", features = ["derive"] }
serde_yaml = "0.9"
Expand Down
130 changes: 126 additions & 4 deletions src/builder.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::{
execute::{CodeExecuter, ExecutionHandle, ExecutionState, ProcessStatus},
markdown::{
elements::{
Code, ListItem, ListItemType, MarkdownElement, ParagraphElement, StyledText, Table, TableRow, Text,
Expand All @@ -7,7 +8,7 @@ use crate::{
},
presentation::{
AsRenderOperations, MarginProperties, PreformattedLine, Presentation, PresentationMetadata,
PresentationThemeMetadata, RenderOperation, Slide,
PresentationThemeMetadata, RenderOnDemand, RenderOnDemandState, RenderOperation, Slide,
},
render::{
highlighting::{CodeHighlighter, CodeLine},
Expand All @@ -17,6 +18,7 @@ use crate::{
style::{Colors, TextStyle},
theme::{Alignment, AuthorPositioning, ElementType, FooterStyle, LoadThemeError, Margin, PresentationTheme},
};
use itertools::Itertools;
use serde::Deserialize;
use std::{borrow::Cow, cell::RefCell, iter, mem, path::PathBuf, rc::Rc, str::FromStr};
use unicode_width::UnicodeWidthStr;
Expand Down Expand Up @@ -428,12 +430,12 @@ impl<'a> PresentationBuilder<'a> {
}

fn push_code(&mut self, code: Code) {
let Code { contents, language, .. } = code;
let Code { contents, language, flags } = code;
let mut code = String::new();
let horizontal_padding = self.theme.code.padding.horizontal.unwrap_or(0);
let vertical_padding = self.theme.code.padding.vertical.unwrap_or(0);
if horizontal_padding == 0 && vertical_padding == 0 {
code = contents;
code = contents.clone();
} else {
if vertical_padding > 0 {
code.push('\n');
Expand Down Expand Up @@ -465,6 +467,19 @@ impl<'a> PresentationBuilder<'a> {
}));
self.push_line_break();
}
if flags.execute {
self.push_code_execution(Code { contents, language, flags });
}
}

fn push_code_execution(&mut self, code: Code) {
let operation = RunCodeOperation::new(
code,
self.theme.default_style.colors.clone(),
self.theme.execution_output.colors.clone(),
);
let operation = RenderOperation::RenderOnDemand(Rc::new(operation));
self.slide_operations.push(operation);
}

fn terminate_slide(&mut self, mode: TerminateMode) {
Expand Down Expand Up @@ -701,6 +716,112 @@ impl FromStr for CommentCommand {
#[error("invalid command: {0}")]
pub struct CommandParseError(#[from] serde_yaml::Error);

#[derive(Debug)]
struct RunCodeOperationInner {
handle: Option<ExecutionHandle>,
output_lines: Vec<String>,
state: RenderOnDemandState,
}

#[derive(Debug)]
pub struct RunCodeOperation {
code: Code,
default_colors: Colors,
block_colors: Colors,
inner: Rc<RefCell<RunCodeOperationInner>>,
}

impl RunCodeOperation {
fn new(code: Code, default_colors: Colors, block_colors: Colors) -> Self {
let inner =
RunCodeOperationInner { handle: None, output_lines: Vec::new(), state: RenderOnDemandState::default() };
Self { code, default_colors, block_colors, inner: Rc::new(RefCell::new(inner)) }
}

fn render_line(&self, line: String) -> RenderOperation {
let line_len = line.len();
RenderOperation::RenderPreformattedLine(PreformattedLine {
text: line,
unformatted_length: line_len,
block_length: line_len,
alignment: Default::default(),
})
}
}

impl AsRenderOperations for RunCodeOperation {
fn as_render_operations(&self, dimensions: &WindowSize) -> Vec<RenderOperation> {
let inner = self.inner.borrow();
if matches!(inner.state, RenderOnDemandState::NotStarted) {
return Vec::new();
}
let state = match inner.state {
RenderOnDemandState::Rendered => "done",
_ => "running",
};
let heading = format!(" [{state}] ");
// TODO remove `RenderSeparator` and turn it into a dynamic operation with optional heading
let dashes_len = (dimensions.columns as usize).saturating_sub(heading.len()) / 2;
let dashes = "—".repeat(dashes_len);
let separator = format!("{dashes}{heading}{dashes}");
let mut operations = vec![
RenderOperation::RenderLineBreak,
self.render_line(separator),
RenderOperation::RenderLineBreak,
RenderOperation::RenderLineBreak,
RenderOperation::SetColors(self.block_colors.clone()),
];

for line in &inner.output_lines {
let chunks = line.chars().chunks(dimensions.columns as usize);
for chunk in &chunks {
operations.push(self.render_line(chunk.collect()));
operations.push(RenderOperation::RenderLineBreak);
}
}
operations.push(RenderOperation::SetColors(self.default_colors.clone()));
operations
}
}

impl RenderOnDemand for RunCodeOperation {
fn poll_state(&self) -> RenderOnDemandState {
let mut inner = self.inner.borrow_mut();
if let Some(handle) = inner.handle.as_mut() {
let state = handle.state();
let ExecutionState { output, status } = state;
if status.is_finished() {
inner.handle.take();
inner.state = RenderOnDemandState::Rendered;
}
inner.output_lines = output;
if matches!(status, ProcessStatus::Failure) {
inner.output_lines.push("[finished with error]".to_string());
}
}
inner.state.clone()
}

fn start_render(&self) -> bool {
let mut inner = self.inner.borrow_mut();
if !matches!(inner.state, RenderOnDemandState::NotStarted) {
return false;
}
match CodeExecuter::execute(&self.code) {
Ok(handle) => {
inner.handle = Some(handle);
inner.state = RenderOnDemandState::Rendering;
true
}
Err(e) => {
inner.output_lines = vec![e.to_string()];
inner.state = RenderOnDemandState::Rendered;
true
}
}
}
}

#[cfg(test)]
mod test {
use rstest::rstest;
Expand Down Expand Up @@ -753,7 +874,8 @@ mod test {
| RenderLineBreak
| RenderImage(_)
| RenderPreformattedLine(_)
| RenderDynamic(_) => true,
| RenderDynamic(_)
| RenderOnDemand(_) => true,
}
}

Expand Down
26 changes: 13 additions & 13 deletions src/execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ pub enum CodeExecuteError {
}

/// A handle for the execution of a piece of code.
#[derive(Debug)]
pub struct ExecutionHandle {
state: Arc<Mutex<ExecutionState>>,
#[allow(dead_code)]
Expand Down Expand Up @@ -100,10 +101,7 @@ impl ProcessReader {
let stdout = BufReader::new(stdout);
let _ = Self::process_output(self.state.clone(), stdout);
let success = match self.handle.try_wait() {
Ok(Some(code)) => {
println!("Exit code {code:?}");
code.success()
}
Ok(Some(code)) => code.success(),
_ => false,
};
let status = match success {
Expand All @@ -124,18 +122,13 @@ impl ProcessReader {
}

/// The state of the execution of a process.
#[derive(Clone, Default)]
#[derive(Clone, Default, Debug)]
pub struct ExecutionState {
output: Vec<String>,
status: ProcessStatus,
pub output: Vec<String>,
pub status: ProcessStatus,
}

impl ExecutionState {
/// Check whether the underlying process is finished.
pub fn is_finished(&self) -> bool {
matches!(self.status, ProcessStatus::Success | ProcessStatus::Failure)
}

/// Extract the lines printed so far.
pub fn into_lines(self) -> Vec<String> {
self.output
Expand All @@ -151,6 +144,13 @@ pub enum ProcessStatus {
Failure,
}

impl ProcessStatus {
/// Check whether the underlying process is finished.
pub fn is_finished(&self) -> bool {
matches!(self, ProcessStatus::Success | ProcessStatus::Failure)
}
}

#[cfg(test)]
mod test {
use super::*;
Expand All @@ -166,7 +166,7 @@ echo 'bye'"
let handle = CodeExecuter::execute(&code).expect("execution failed");
let state = loop {
let state = handle.state();
if state.is_finished() {
if state.status.is_finished() {
break state;
}
};
Expand Down
29 changes: 18 additions & 11 deletions src/input/source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,27 @@ impl CommandSource {
/// Block until the next command arrives.
pub fn next_command(&mut self) -> io::Result<Command> {
loop {
match self.user_input.poll_next_command(Duration::from_millis(250)) {
Ok(Some(command)) => {
return Ok(Command::User(command));
}
Ok(None) => (),
Err(e) => {
return Ok(Command::Abort { error: e.to_string() });
}
};
if self.watcher.has_modifications()? {
return Ok(Command::ReloadPresentation);
if let Some(command) = self.try_next_command()? {
return Ok(command);
}
}
}

/// Try to get the next command.
///
/// This attempts to get a command and returns `Ok(None)` on timeout.
pub fn try_next_command(&mut self) -> io::Result<Option<Command>> {
match self.user_input.poll_next_command(Duration::from_millis(250)) {
Ok(Some(command)) => {
return Ok(Some(Command::User(command)));
}
Ok(None) => (),
Err(e) => {
return Ok(Some(Command::Abort { error: e.to_string() }));
}
};
if self.watcher.has_modifications()? { Ok(Some(Command::ReloadPresentation)) } else { Ok(None) }
}
}

/// A command.
Expand Down
6 changes: 6 additions & 0 deletions src/input/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ impl UserInput {
KeyCode::Char('c') if event.modifiers == KeyModifiers::CONTROL => {
(Some(UserCommand::Exit), InputState::Empty)
}
KeyCode::Char('e') if event.modifiers == KeyModifiers::CONTROL => {
(Some(UserCommand::RenderWidgets), InputState::Empty)
}
KeyCode::Char('G') => Self::apply_uppercase_g(state),
KeyCode::Char('g') => Self::apply_lowercase_g(state),
KeyCode::Char(number) if number.is_ascii_digit() => {
Expand Down Expand Up @@ -105,6 +108,9 @@ pub enum UserCommand {
/// Jump to one particular slide.
JumpSlide(u32),

/// Render any widgets in the currently visible slide.
RenderWidgets,

/// Exit the presentation.
Exit,
}
Expand Down
Loading

0 comments on commit 8b2aadf

Please sign in to comment.