Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add executable blocks #17

Merged
merged 4 commits into from
Oct 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ 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"
serde_with = "3.3"
syntect = "5.1"
strum = { version = "0.25", features = ["derive"] }
tempfile = "3.8"
thiserror = "1"
unicode-width = "0.1"
viuer = "0.7.1"
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ nix run github:mfontanini/presenterm
* Configurable [column layouts](/docs/layouts.md) that let you structure parts of your slide into columns.
* Support for an introduction slide that displays the presentation title and your name.
* Support for slide titles.
* Support for shell code execution.
* Create pauses in between each slide so that it progressively renders for a more interactive presentation.
* Text formatting support for **bold**, _italics_, ~strikethrough~, and `inline code`.
* Automatically reload your presentation every time it changes for a fast development loop.
Expand Down Expand Up @@ -188,6 +189,15 @@ them to be, and then put any content into them. For example:

See the [documentation](/docs/layouts.md) on layouts to learn more.

## Shell code execution

> **Note**: this is available in the `master` branch and in the upcoming 0.3.0 version.

Any shell code can be marked for execution, making _presenterm_ execute it and render its output when you press ctrl+e.
In order to do this, annotate the code block with `+exec` (e.g. `bash +exec`). **Obviously use this at your own risk!**

[![asciicast](https://asciinema.org/a/1v3IqCEtU9tqDjVj78Pp7SSe2.svg)](https://asciinema.org/a/1v3IqCEtU9tqDjVj78Pp7SSe2)

## Navigation

Navigation should be intuitive: jumping to the next/previous slide can be done by using the arrow, _hjkl_, and page
Expand Down
138 changes: 132 additions & 6 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,12 +716,118 @@ 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;

use super::*;
use crate::{markdown::elements::ProgrammingLanguage, presentation::PreformattedLine};
use crate::{markdown::elements::CodeLanguage, presentation::PreformattedLine};

fn build_presentation(elements: Vec<MarkdownElement>) -> Presentation {
try_build_presentation(elements).expect("build failed")
Expand Down Expand Up @@ -753,7 +874,8 @@ mod test {
| RenderLineBreak
| RenderImage(_)
| RenderPreformattedLine(_)
| RenderDynamic(_) => true,
| RenderDynamic(_)
| RenderOnDemand(_) => true,
}
}

Expand Down Expand Up @@ -817,7 +939,11 @@ mod test {
let text = "苹果".to_string();
let elements = vec![
MarkdownElement::BlockQuote(vec![text.clone()]),
MarkdownElement::Code(Code { contents: text.clone(), language: ProgrammingLanguage::Unknown }),
MarkdownElement::Code(Code {
contents: text.clone(),
language: CodeLanguage::Unknown,
flags: Default::default(),
}),
];
let presentation = build_presentation(elements);
let slides = presentation.into_slides();
Expand Down
Loading
Loading