mirror of
https://github.com/mfontanini/presenterm.git
synced 2025-05-05 23:42:59 +00:00
feat: allow executing arbitrary programming langs
This commit is contained in:
parent
95ee33f1f2
commit
e1e88c780a
41
build.rs
41
build.rs
@ -6,13 +6,13 @@ use std::{
|
||||
|
||||
// Take all files under `themes` and turn them into a file that contains a hashmap with their
|
||||
// contents by name. This is pulled in theme.rs to construct themes.
|
||||
fn main() -> io::Result<()> {
|
||||
let out_dir = env::var("OUT_DIR").unwrap();
|
||||
fn build_themes(out_dir: &str) -> io::Result<()> {
|
||||
let output_path = format!("{out_dir}/themes.rs");
|
||||
let mut output_file = BufWriter::new(File::create(output_path)?);
|
||||
output_file.write_all(b"use std::collections::BTreeMap as Map;\n")?;
|
||||
output_file.write_all(b"use once_cell::sync::Lazy;\n")?;
|
||||
output_file.write_all(b"static THEMES: Lazy<Map<&'static str, &'static [u8]>> = Lazy::new(|| Map::from([\n")?;
|
||||
|
||||
let mut paths = fs::read_dir("themes")?.collect::<io::Result<Vec<_>>>()?;
|
||||
paths.sort_by_key(|e| e.path());
|
||||
for theme_file in paths {
|
||||
@ -33,3 +33,40 @@ fn main() -> io::Result<()> {
|
||||
println!("cargo:rerun-if-changed=themes");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_executors(out_dir: &str) -> io::Result<()> {
|
||||
let output_path = format!("{out_dir}/executors.rs");
|
||||
let mut output_file = BufWriter::new(File::create(output_path)?);
|
||||
output_file.write_all(b"use std::collections::BTreeMap as Map;\n")?;
|
||||
output_file.write_all(b"use once_cell::sync::Lazy;\n")?;
|
||||
output_file.write_all(b"static EXECUTORS: Lazy<Map<crate::markdown::elements::CodeLanguage, &'static [u8]>> = Lazy::new(|| Map::from([\n")?;
|
||||
|
||||
let mut paths = fs::read_dir("executors")?.collect::<io::Result<Vec<_>>>()?;
|
||||
paths.sort_by_key(|e| e.path());
|
||||
for file in paths {
|
||||
let metadata = file.metadata()?;
|
||||
if !metadata.is_file() {
|
||||
panic!("found non file in executors directory");
|
||||
}
|
||||
let path = file.path();
|
||||
let contents = fs::read(&path)?;
|
||||
let file_name = path.file_name().unwrap().to_string_lossy();
|
||||
let executor_name = file_name.split_once('.').unwrap().0;
|
||||
output_file.write_all(
|
||||
format!("(crate::markdown::elements::CodeLanguage::{executor_name}, {contents:?}.as_slice()),\n")
|
||||
.as_bytes(),
|
||||
)?;
|
||||
}
|
||||
output_file.write_all(b"]));\n")?;
|
||||
|
||||
// Rebuild if anything changes.
|
||||
println!("cargo:rerun-if-changed=executors");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> io::Result<()> {
|
||||
let out_dir = env::var("OUT_DIR").unwrap();
|
||||
build_themes(&out_dir)?;
|
||||
build_executors(&out_dir)?;
|
||||
Ok(())
|
||||
}
|
||||
|
3
executors/Python.sh
Normal file
3
executors/Python.sh
Normal file
@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
exec python "$1"
|
@ -1,4 +1,5 @@
|
||||
use crate::{
|
||||
execute::CodeExecuter,
|
||||
input::{
|
||||
source::Command,
|
||||
user::{CommandKeyBindings, UserInput},
|
||||
@ -101,11 +102,13 @@ impl<W: TerminalWrite> ThemesDemo<W> {
|
||||
let mut resources = Resources::new("non_existent", image_registry.clone());
|
||||
let mut typst = TypstRender::default();
|
||||
let options = PresentationBuilderOptions::default();
|
||||
let executer = CodeExecuter;
|
||||
let bindings_config = Default::default();
|
||||
let builder = PresentationBuilder::new(
|
||||
theme,
|
||||
&mut resources,
|
||||
&mut typst,
|
||||
&executer,
|
||||
&self.themes,
|
||||
image_registry,
|
||||
bindings_config,
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
use crate::markdown::elements::{Code, CodeLanguage};
|
||||
use std::{
|
||||
ffi::OsStr,
|
||||
io::{self, BufRead, BufReader, Write},
|
||||
process::{self, Stdio},
|
||||
sync::{Arc, Mutex},
|
||||
@ -9,37 +10,46 @@ use std::{
|
||||
};
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/executors.rs"));
|
||||
|
||||
/// Allows executing code.
|
||||
pub(crate) struct CodeExecuter;
|
||||
pub struct CodeExecuter;
|
||||
|
||||
impl CodeExecuter {
|
||||
pub(crate) fn is_execution_supported(&self, language: &CodeLanguage) -> bool {
|
||||
matches!(language, CodeLanguage::Shell(_))
|
||||
if matches!(language, CodeLanguage::Shell(_)) { true } else { EXECUTORS.contains_key(language) }
|
||||
}
|
||||
|
||||
/// Execute a piece of code.
|
||||
pub(crate) fn execute(&self, code: &Code) -> Result<ExecutionHandle, CodeExecuteError> {
|
||||
if !self.is_execution_supported(&code.language) {
|
||||
return Err(CodeExecuteError::UnsupportedExecution);
|
||||
}
|
||||
if !code.attributes.execute {
|
||||
return Err(CodeExecuteError::NotExecutableCode);
|
||||
}
|
||||
match &code.language {
|
||||
CodeLanguage::Shell(interpreter) => Self::execute_shell(interpreter, &code.contents),
|
||||
_ => Err(CodeExecuteError::UnsupportedExecution),
|
||||
CodeLanguage::Shell(interpreter) => {
|
||||
let args: &[&str] = &[];
|
||||
Self::execute_shell(interpreter, code.contents.as_bytes(), args)
|
||||
}
|
||||
lang => {
|
||||
let executor = EXECUTORS.get(lang).ok_or(CodeExecuteError::UnsupportedExecution)?;
|
||||
Self::execute_lang(executor, code.contents.as_bytes())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn execute_shell(interpreter: &str, code: &str) -> Result<ExecutionHandle, CodeExecuteError> {
|
||||
fn execute_shell<S>(interpreter: &str, code: &[u8], args: &[S]) -> Result<ExecutionHandle, CodeExecuteError>
|
||||
where
|
||||
S: AsRef<OsStr>,
|
||||
{
|
||||
let mut output_file = NamedTempFile::new().map_err(CodeExecuteError::TempFile)?;
|
||||
output_file.write_all(code.as_bytes()).map_err(CodeExecuteError::TempFile)?;
|
||||
output_file.write_all(code).map_err(CodeExecuteError::TempFile)?;
|
||||
output_file.flush().map_err(CodeExecuteError::TempFile)?;
|
||||
let (reader, writer) = os_pipe::pipe().map_err(CodeExecuteError::Pipe)?;
|
||||
let writer_clone = writer.try_clone().map_err(CodeExecuteError::Pipe)?;
|
||||
let process_handle = process::Command::new("/usr/bin/env")
|
||||
.arg(interpreter)
|
||||
.arg(output_file.path())
|
||||
.args(args)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(writer)
|
||||
.stderr(writer_clone)
|
||||
@ -48,7 +58,17 @@ impl CodeExecuter {
|
||||
|
||||
let state: Arc<Mutex<ExecutionState>> = Default::default();
|
||||
let reader_handle = ProcessReader::spawn(process_handle, state.clone(), output_file, reader);
|
||||
let handle = ExecutionHandle { state, reader_handle };
|
||||
let handle = ExecutionHandle { state, reader_handle, program_path: None };
|
||||
Ok(handle)
|
||||
}
|
||||
|
||||
fn execute_lang(executor: &[u8], code: &[u8]) -> Result<ExecutionHandle, CodeExecuteError> {
|
||||
let mut code_file = NamedTempFile::new().map_err(CodeExecuteError::TempFile)?;
|
||||
code_file.write_all(code).map_err(CodeExecuteError::TempFile)?;
|
||||
|
||||
let path = code_file.path();
|
||||
let mut handle = Self::execute_shell("bash", executor, &[path])?;
|
||||
handle.program_path = Some(code_file);
|
||||
Ok(handle)
|
||||
}
|
||||
}
|
||||
@ -78,6 +98,7 @@ pub(crate) struct ExecutionHandle {
|
||||
state: Arc<Mutex<ExecutionState>>,
|
||||
#[allow(dead_code)]
|
||||
reader_handle: thread::JoinHandle<()>,
|
||||
program_path: Option<NamedTempFile>,
|
||||
}
|
||||
|
||||
impl ExecutionHandle {
|
||||
|
@ -1,5 +1,6 @@
|
||||
use crate::{
|
||||
custom::KeyBindingsConfig,
|
||||
execute::CodeExecuter,
|
||||
markdown::parse::ParseError,
|
||||
media::{
|
||||
image::{Image, ImageSource},
|
||||
@ -83,6 +84,7 @@ impl<'a> Exporter<'a> {
|
||||
self.default_theme,
|
||||
&mut self.resources,
|
||||
&mut self.typst,
|
||||
&CodeExecuter,
|
||||
&self.themes,
|
||||
Default::default(),
|
||||
KeyBindingsConfig::default(),
|
||||
|
@ -23,6 +23,7 @@ pub(crate) mod typst;
|
||||
pub use crate::{
|
||||
custom::{Config, ImageProtocol, ValidateOverflows},
|
||||
demo::ThemesDemo,
|
||||
execute::CodeExecuter,
|
||||
export::{ExportError, Exporter},
|
||||
input::source::CommandSource,
|
||||
markdown::parse::MarkdownParser,
|
||||
|
10
src/main.rs
10
src/main.rs
@ -2,9 +2,9 @@ use clap::{error::ErrorKind, CommandFactory, Parser};
|
||||
use comrak::Arena;
|
||||
use directories::ProjectDirs;
|
||||
use presenterm::{
|
||||
CommandSource, Config, Exporter, GraphicsMode, HighlightThemeSet, ImagePrinter, ImageProtocol, ImageRegistry,
|
||||
LoadThemeError, MarkdownParser, PresentMode, PresentationBuilderOptions, PresentationTheme, PresentationThemeSet,
|
||||
Presenter, PresenterOptions, Resources, Themes, ThemesDemo, TypstRender, ValidateOverflows,
|
||||
CodeExecuter, CommandSource, Config, Exporter, GraphicsMode, HighlightThemeSet, ImagePrinter, ImageProtocol,
|
||||
ImageRegistry, LoadThemeError, MarkdownParser, PresentMode, PresentationBuilderOptions, PresentationTheme,
|
||||
PresentationThemeSet, Presenter, PresenterOptions, Resources, Themes, ThemesDemo, TypstRender, ValidateOverflows,
|
||||
};
|
||||
use std::{
|
||||
env, io,
|
||||
@ -206,6 +206,7 @@ fn run(mut cli: Cli) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let registry = ImageRegistry(printer.clone());
|
||||
let resources = Resources::new(resources_path, registry.clone());
|
||||
let typst = TypstRender::new(config.typst.ppi, registry, resources_path);
|
||||
let code_executer = CodeExecuter;
|
||||
if cli.export_pdf || cli.generate_pdf_metadata {
|
||||
let mut exporter = Exporter::new(parser, &default_theme, resources, typst, themes, options);
|
||||
let mut args = Vec::new();
|
||||
@ -232,7 +233,8 @@ fn run(mut cli: Cli) -> Result<(), Box<dyn std::error::Error>> {
|
||||
bindings: config.bindings,
|
||||
validate_overflows,
|
||||
};
|
||||
let presenter = Presenter::new(&default_theme, commands, parser, resources, typst, themes, printer, options);
|
||||
let presenter =
|
||||
Presenter::new(&default_theme, commands, parser, resources, typst, code_executer, themes, printer, options);
|
||||
presenter.present(&path)?;
|
||||
}
|
||||
Ok(())
|
||||
|
@ -186,8 +186,8 @@ pub(crate) struct Code {
|
||||
}
|
||||
|
||||
/// The language of a piece of code.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, EnumIter)]
|
||||
pub(crate) enum CodeLanguage {
|
||||
#[derive(Clone, Debug, PartialEq, Eq, EnumIter, PartialOrd, Ord)]
|
||||
pub enum CodeLanguage {
|
||||
Ada,
|
||||
Asp,
|
||||
Awk,
|
||||
|
@ -1,20 +1,9 @@
|
||||
use crate::{
|
||||
custom::KeyBindingsConfig,
|
||||
diff::PresentationDiffer,
|
||||
export::ImageReplacer,
|
||||
input::source::{Command, CommandSource},
|
||||
markdown::parse::{MarkdownParser, ParseError},
|
||||
media::{printer::ImagePrinter, register::ImageRegistry},
|
||||
presentation::Presentation,
|
||||
processing::builder::{BuildError, PresentationBuilder, PresentationBuilderOptions, Themes},
|
||||
render::{
|
||||
custom::KeyBindingsConfig, diff::PresentationDiffer, execute::CodeExecuter, export::ImageReplacer, input::source::{Command, CommandSource}, markdown::parse::{MarkdownParser, ParseError}, media::{printer::ImagePrinter, register::ImageRegistry}, presentation::Presentation, processing::builder::{BuildError, PresentationBuilder, PresentationBuilderOptions, Themes}, render::{
|
||||
draw::{RenderError, RenderResult, TerminalDrawer},
|
||||
properties::WindowSize,
|
||||
validate::OverflowValidator,
|
||||
},
|
||||
resource::Resources,
|
||||
theme::PresentationTheme,
|
||||
typst::TypstRender,
|
||||
}, resource::Resources, theme::PresentationTheme, typst::TypstRender
|
||||
};
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
@ -43,6 +32,7 @@ pub struct Presenter<'a> {
|
||||
parser: MarkdownParser<'a>,
|
||||
resources: Resources,
|
||||
typst: TypstRender,
|
||||
code_executer: CodeExecuter,
|
||||
state: PresenterState,
|
||||
slides_with_pending_widgets: HashSet<usize>,
|
||||
image_printer: Rc<ImagePrinter>,
|
||||
@ -59,6 +49,7 @@ impl<'a> Presenter<'a> {
|
||||
parser: MarkdownParser<'a>,
|
||||
resources: Resources,
|
||||
typst: TypstRender,
|
||||
code_executer: CodeExecuter,
|
||||
themes: Themes,
|
||||
image_printer: Rc<ImagePrinter>,
|
||||
options: PresenterOptions,
|
||||
@ -69,6 +60,7 @@ impl<'a> Presenter<'a> {
|
||||
parser,
|
||||
resources,
|
||||
typst,
|
||||
code_executer,
|
||||
state: PresenterState::Empty,
|
||||
slides_with_pending_widgets: HashSet::new(),
|
||||
image_printer,
|
||||
@ -257,6 +249,7 @@ impl<'a> Presenter<'a> {
|
||||
self.default_theme,
|
||||
&mut self.resources,
|
||||
&mut self.typst,
|
||||
&self.code_executer,
|
||||
&self.themes,
|
||||
ImageRegistry(self.image_printer.clone()),
|
||||
self.options.bindings.clone(),
|
||||
|
@ -1,5 +1,6 @@
|
||||
use crate::{
|
||||
custom::{KeyBindingsConfig, OptionsConfig},
|
||||
execute::CodeExecuter,
|
||||
markdown::{
|
||||
elements::{
|
||||
Code, CodeLanguage, Highlight, HighlightGroup, ListItem, ListItemType, MarkdownElement, ParagraphElement,
|
||||
@ -94,6 +95,7 @@ pub(crate) struct PresentationBuilder<'a> {
|
||||
chunk_mutators: Vec<Box<dyn ChunkMutator>>,
|
||||
slides: Vec<Slide>,
|
||||
highlighter: CodeHighlighter,
|
||||
code_executer: &'a CodeExecuter,
|
||||
theme: Cow<'a, PresentationTheme>,
|
||||
resources: &'a mut Resources,
|
||||
typst: &'a mut TypstRender,
|
||||
@ -113,6 +115,7 @@ impl<'a> PresentationBuilder<'a> {
|
||||
default_theme: &'a PresentationTheme,
|
||||
resources: &'a mut Resources,
|
||||
typst: &'a mut TypstRender,
|
||||
code_executer: &'a CodeExecuter,
|
||||
themes: &'a Themes,
|
||||
image_registry: ImageRegistry,
|
||||
bindings_config: KeyBindingsConfig,
|
||||
@ -124,6 +127,7 @@ impl<'a> PresentationBuilder<'a> {
|
||||
chunk_mutators: Vec::new(),
|
||||
slides: Vec::new(),
|
||||
highlighter: CodeHighlighter::default(),
|
||||
code_executer,
|
||||
theme: Cow::Borrowed(default_theme),
|
||||
resources,
|
||||
typst,
|
||||
@ -680,7 +684,7 @@ impl<'a> PresentationBuilder<'a> {
|
||||
self.chunk_mutators.push(Box::new(HighlightMutator::new(context)));
|
||||
}
|
||||
if code.attributes.execute {
|
||||
self.push_code_execution(code);
|
||||
self.push_code_execution(code)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@ -728,7 +732,10 @@ impl<'a> PresentationBuilder<'a> {
|
||||
(output, context)
|
||||
}
|
||||
|
||||
fn push_code_execution(&mut self, code: Code) {
|
||||
fn push_code_execution(&mut self, code: Code) -> Result<(), BuildError> {
|
||||
if !self.code_executer.is_execution_supported(&code.language) {
|
||||
return Err(BuildError::UnsupportedExecution(code.language));
|
||||
}
|
||||
let operation = RunCodeOperation::new(
|
||||
code,
|
||||
self.theme.default_style.colors.clone(),
|
||||
@ -736,6 +743,7 @@ impl<'a> PresentationBuilder<'a> {
|
||||
);
|
||||
let operation = RenderOperation::RenderOnDemand(Rc::new(operation));
|
||||
self.chunk_operations.push(operation);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn terminate_slide(&mut self) {
|
||||
@ -898,6 +906,9 @@ pub enum BuildError {
|
||||
|
||||
#[error("typst render failed: {0}")]
|
||||
TypstRender(#[from] TypstRenderError),
|
||||
|
||||
#[error("language {0:?} does not support execution")]
|
||||
UnsupportedExecution(CodeLanguage),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||
@ -1050,12 +1061,14 @@ mod test {
|
||||
let theme = PresentationTheme::default();
|
||||
let mut resources = Resources::new("/tmp", Default::default());
|
||||
let mut typst = TypstRender::default();
|
||||
let code_executer = CodeExecuter;
|
||||
let themes = Themes::default();
|
||||
let bindings = KeyBindingsConfig::default();
|
||||
let builder = PresentationBuilder::new(
|
||||
&theme,
|
||||
&mut resources,
|
||||
&mut typst,
|
||||
&code_executer,
|
||||
&themes,
|
||||
Default::default(),
|
||||
bindings,
|
||||
|
Loading…
x
Reference in New Issue
Block a user