feat: make snippet executors multiplatform

This commit is contained in:
Matias Fontanini 2024-07-12 16:53:31 -07:00
parent 480f9475cc
commit a2df29ddbe
27 changed files with 285 additions and 261 deletions

View File

@ -34,39 +34,8 @@ fn build_themes(out_dir: &str) -> io::Result<()> {
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 (language, extension) = file_name.split_once('.').unwrap();
if extension != "sh" {
panic!("extension must be 'sh'");
}
output_file.write_all(format!("(\"{language}\".parse().unwrap(), {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(())
}

View File

@ -211,6 +211,38 @@
},
"additionalProperties": false
},
"LanguageSnippetExecutionConfig": {
"description": "The snippet execution configuration for a specific programming language.",
"type": "object",
"required": [
"commands",
"filename"
],
"properties": {
"commands": {
"description": "The commands to be run when executing snippets for this programming language.",
"type": "array",
"items": {
"type": "array",
"items": {
"type": "string"
}
}
},
"environment": {
"description": "The environment variables to set before invoking every command.",
"default": {},
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"filename": {
"description": "The filename to use for the snippet input file.",
"type": "string"
}
}
},
"MermaidConfig": {
"type": "object",
"properties": {
@ -293,6 +325,13 @@
"enable"
],
"properties": {
"custom": {
"description": "Custom snippet executors.",
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/LanguageSnippetExecutionConfig"
}
},
"enable": {
"description": "Whether to enable snippet execution.",
"type": "boolean"

73
executors.yaml Normal file
View File

@ -0,0 +1,73 @@
bash:
filename: script.sh
commands:
- ["bash", "script.sh"]
c++:
filename: snippet.cpp
commands:
- ["g++", "-std=c++20", "snippet.cpp", "-o", "snippet"]
- ["./snippet"]
c:
filename: snippet.c
commands:
- ["gcc", "snippet.c", "-o", "snippet"]
- ["./snippet"]
fish:
filename: script.fish
commands:
- ["fish", "script.fish"]
go:
filename: snippet.go
environment:
GO11MODULE: off
commands:
- ["go", "run", "snippet.go"]
java:
filename: Snippet.java
commands:
- ["java", "Snippet.java"]
js:
filename: snippet.js
commands:
- ["node", "snippet.js"]
kotlin:
filename: snippet.kts
commands:
- ["kotlinc", "-script", "script.kts"]
lua:
filename: snippet.lua
commands:
- ["lua", "snippet.lua"]
nushell:
filename: snippet.nu
commands:
- ["nu", "snippet.nu"]
perl:
filename: snippet.pl
commands:
- ["perl", "snippet.pl"]
python:
filename: snippet.py
commands:
- ["python", "-u", "snippet.py"]
ruby:
filename: snippet.rb
commands:
- ["ruby", "snippet.rb"]
rust-script:
filename: snippet.rs
commands:
- ["rust-script", "snippet.rs"]
rust:
filename: snippet.rs
commands:
- ["rustc", "--crate-name", "presenterm_snippet", "snippet.rs", "-o", "snippet"]
- ["./snippet"]
sh:
filename: script.sh
commands:
- ["sh", "script.sh"]
zsh:
filename: script.sh
commands:
- ["zsh", "script.sh"]

View File

@ -1,5 +0,0 @@
#!/usr/bin/env bash
temp=$(mktemp)
g++ -std=c++20 -x c++ "$1" -o "$temp"
"$temp"

View File

@ -1,5 +0,0 @@
#!/usr/bin/env bash
temp=$(mktemp)
gcc -x c "$1" -o "$temp"
"$temp"

View File

@ -1,8 +0,0 @@
#!/usr/bin/env bash
export GO111MODULE=off
tempdir=$(mktemp -d)
cd "$tempdir"
mv "$1" main.go
go run main.go
rm -rf "$tempdir"

View File

@ -1,7 +0,0 @@
#!/usr/bin/env bash
tempdir=$(mktemp -d)
cd "$tempdir"
cp "$1" Main.java
java Main.java
rm -rf "$tempdir"

View File

@ -1,3 +0,0 @@
#!/usr/bin/env bash
node "$1"

View File

@ -1,7 +0,0 @@
#!/usr/bin/env bash
tempdir=$(mktemp -d)
cd "$tempdir"
cp "$1" script.kts
kotlinc -script script.kts
rm -rf "$tempdir"

View File

@ -1,3 +0,0 @@
#!/usr/bin/env bash
lua "$1"

View File

@ -1,3 +0,0 @@
#!/usr/bin/env bash
nu "$1"

View File

@ -1,3 +0,0 @@
#!/usr/bin/env bash
perl "$1"

View File

@ -1,3 +0,0 @@
#!/usr/bin/env bash
exec python -u "$1"

View File

@ -1,3 +0,0 @@
#!/usr/bin/env bash
ruby "$1"

View File

@ -1,3 +0,0 @@
#!/usr/bin/env bash
rust-script "$1"

View File

@ -1,5 +0,0 @@
#!/usr/bin/env bash
temp=$(mktemp)
rustc --crate-name "presenterm_snippet" "$1" -o "$temp"
"$temp"

View File

@ -27,7 +27,7 @@
"src"
"themes"
"bat"
"executors"
"executors.yaml"
];
buildSrc = flakeboxLib.filterSubPaths {

View File

@ -1,12 +1,17 @@
use crate::{
input::user::KeyBinding,
markdown::elements::CodeLanguage,
media::{emulator::TerminalEmulator, kitty::KittyMode},
GraphicsMode,
};
use clap::ValueEnum;
use schemars::JsonSchema;
use serde::Deserialize;
use std::{fs, io, path::Path};
use std::{
collections::{BTreeMap, HashMap},
fs, io,
path::Path,
};
#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
@ -134,6 +139,10 @@ pub struct SnippetConfig {
pub struct SnippetExecConfig {
/// Whether to enable snippet execution.
pub enable: bool,
/// Custom snippet executors.
#[serde(default)]
pub custom: BTreeMap<CodeLanguage, LanguageSnippetExecutionConfig>,
}
#[derive(Clone, Debug, Deserialize, JsonSchema)]
@ -190,6 +199,20 @@ pub(crate) fn default_mermaid_scale() -> u32 {
2
}
/// The snippet execution configuration for a specific programming language.
#[derive(Clone, Debug, Deserialize, JsonSchema)]
pub struct LanguageSnippetExecutionConfig {
/// The filename to use for the snippet input file.
pub filename: String,
/// The environment variables to set before invoking every command.
#[serde(default)]
pub environment: HashMap<String, String>,
/// The commands to be run when executing snippets for this programming language.
pub commands: Vec<Vec<String>>,
}
#[derive(Clone, Debug, Default, Deserialize, ValueEnum, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum ImageProtocol {

View File

@ -1,5 +1,5 @@
use crate::{
execute::CodeExecutor,
execute::SnippetExecutor,
input::{
source::Command,
user::{CommandKeyBindings, UserInput},
@ -102,7 +102,7 @@ impl<W: TerminalWrite> ThemesDemo<W> {
let mut resources = Resources::new("non_existent", image_registry.clone());
let mut third_party = ThirdPartyRender::default();
let options = PresentationBuilderOptions::default();
let executer = Rc::new(CodeExecutor::default());
let executer = Rc::new(SnippetExecutor::default());
let bindings_config = Default::default();
let builder = PresentationBuilder::new(
theme,

View File

@ -1,60 +1,54 @@
//! Code execution.
use crate::markdown::elements::{Code, CodeLanguage};
use crate::{
custom::LanguageSnippetExecutionConfig,
markdown::elements::{Code, CodeLanguage},
};
use once_cell::sync::Lazy;
use os_pipe::PipeReader;
use std::{
collections::BTreeMap,
ffi::OsStr,
fs,
collections::{BTreeMap, HashMap},
fs::File,
io::{self, BufRead, BufReader, Write},
path::{Path, PathBuf},
process::{self, Stdio},
process::{self, Child, Stdio},
sync::{Arc, Mutex},
thread,
};
use tempfile::NamedTempFile;
use tempfile::TempDir;
include!(concat!(env!("OUT_DIR"), "/executors.rs"));
static EXECUTORS: Lazy<BTreeMap<CodeLanguage, LanguageSnippetExecutionConfig>> =
Lazy::new(|| serde_yaml::from_slice(include_bytes!("../executors.yaml")).expect("executors.yaml is broken"));
/// Allows executing code.
#[derive(Default, Debug)]
pub struct CodeExecutor {
custom_executors: BTreeMap<CodeLanguage, Vec<u8>>,
#[derive(Debug)]
pub struct SnippetExecutor {
executors: BTreeMap<CodeLanguage, LanguageSnippetExecutionConfig>,
}
impl CodeExecutor {
pub fn load(executors_path: &Path) -> Result<Self, LoadExecutorsError> {
let mut custom_executors = BTreeMap::new();
if let Ok(paths) = fs::read_dir(executors_path) {
for executor in paths {
let executor = executor?;
let path = executor.path();
let filename = path.file_name().unwrap_or_default().to_string_lossy();
let Some((name, extension)) = filename.split_once('.') else {
return Err(LoadExecutorsError::InvalidExecutor(path, "no extension"));
};
if extension != "sh" {
return Err(LoadExecutorsError::InvalidExecutor(path, "non .sh extension"));
impl SnippetExecutor {
pub fn new(
custom_executors: BTreeMap<CodeLanguage, LanguageSnippetExecutionConfig>,
) -> Result<Self, InvalidSnippetConfig> {
let mut executors = EXECUTORS.clone();
executors.extend(custom_executors);
for (language, config) in &executors {
if config.filename.is_empty() {
return Err(InvalidSnippetConfig(language.clone(), "filename is empty"));
}
if config.commands.is_empty() {
return Err(InvalidSnippetConfig(language.clone(), "no commands given"));
}
for command in &config.commands {
if command.is_empty() {
return Err(InvalidSnippetConfig(language.clone(), "empty command given"));
}
let language: CodeLanguage = match name.parse() {
Ok(CodeLanguage::Unknown(_)) => {
return Err(LoadExecutorsError::InvalidExecutor(path, "unknown language"));
}
Ok(language) => language,
Err(_) => return Err(LoadExecutorsError::InvalidExecutor(path, "invalid code language")),
};
let file_contents = fs::read(path)?;
custom_executors.insert(language, file_contents);
}
}
Ok(Self { custom_executors })
Ok(Self { executors })
}
pub(crate) fn is_execution_supported(&self, language: &CodeLanguage) -> bool {
if matches!(language, CodeLanguage::Shell(_)) {
true
} else {
EXECUTORS.contains_key(language) || self.custom_executors.contains_key(language)
}
self.executors.contains_key(language)
}
/// Execute a piece of code.
@ -62,70 +56,39 @@ impl CodeExecutor {
if !code.attributes.execute {
return Err(CodeExecuteError::NotExecutableCode);
}
match &code.language {
CodeLanguage::Shell(interpreter) => {
let args: &[&str] = &[];
Self::execute_shell(interpreter, code.executable_contents().as_bytes(), args)
}
lang => {
let executor = self.executor(lang).ok_or(CodeExecuteError::UnsupportedExecution)?;
Self::execute_lang(executor, code.executable_contents().as_bytes())
}
}
let Some(config) = self.executors.get(&code.language) else {
return Err(CodeExecuteError::UnsupportedExecution);
};
Self::execute_lang(config, code.executable_contents().as_bytes())
}
fn executor(&self, language: &CodeLanguage) -> Option<&[u8]> {
if let Some(executor) = self.custom_executors.get(language) {
return Some(executor);
fn execute_lang(config: &LanguageSnippetExecutionConfig, code: &[u8]) -> Result<ExecutionHandle, CodeExecuteError> {
let script_dir =
tempfile::Builder::default().prefix(".presenterm").tempdir().map_err(CodeExecuteError::TempDir)?;
let snippet_path = script_dir.path().join(&config.filename);
{
let mut snippet_file = File::create(snippet_path).map_err(CodeExecuteError::TempDir)?;
snippet_file.write_all(code).map_err(CodeExecuteError::TempDir)?;
}
EXECUTORS.get(language).copied()
}
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).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)
.spawn()
.map_err(CodeExecuteError::SpawnProcess)?;
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, 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);
let reader_handle =
CommandsRunner::spawn(state.clone(), script_dir, config.commands.clone(), config.environment.clone());
let handle = ExecutionHandle { state, reader_handle };
Ok(handle)
}
}
/// An error during the load of custom executors.
impl Default for SnippetExecutor {
fn default() -> Self {
Self::new(Default::default()).expect("initialization failed")
}
}
/// An invalid executor was found.
#[derive(thiserror::Error, Debug)]
pub enum LoadExecutorsError {
#[error("io: {0}")]
Io(#[from] io::Error),
#[error("invalid executor '{0}': {1}")]
InvalidExecutor(PathBuf, &'static str),
}
#[error("invalid snippet execution for '{0:?}': {1}")]
pub struct InvalidSnippetConfig(CodeLanguage, &'static str);
/// An error during the execution of some code.
#[derive(thiserror::Error, Debug)]
@ -136,11 +99,11 @@ pub(crate) enum CodeExecuteError {
#[error("code is not marked for execution")]
NotExecutableCode,
#[error("error creating temporary file: {0}")]
TempFile(io::Error),
#[error("error creating temporary directory: {0}")]
TempDir(io::Error),
#[error("error spawning process: {0}")]
SpawnProcess(io::Error),
#[error("error spawning process '{0}': {1}")]
SpawnProcess(String, io::Error),
#[error("error creating pipe: {0}")]
Pipe(io::Error),
@ -152,7 +115,6 @@ pub(crate) struct ExecutionHandle {
state: Arc<Mutex<ExecutionState>>,
#[allow(dead_code)]
reader_handle: thread::JoinHandle<()>,
program_path: Option<NamedTempFile>,
}
impl ExecutionHandle {
@ -163,38 +125,75 @@ impl ExecutionHandle {
}
/// Consumes the output of a process and stores it in a shared state.
struct ProcessReader {
handle: process::Child,
struct CommandsRunner {
state: Arc<Mutex<ExecutionState>>,
#[allow(dead_code)]
file_handle: NamedTempFile,
reader: os_pipe::PipeReader,
script_directory: TempDir,
}
impl ProcessReader {
impl CommandsRunner {
fn spawn(
handle: process::Child,
state: Arc<Mutex<ExecutionState>>,
file_handle: NamedTempFile,
reader: os_pipe::PipeReader,
script_directory: TempDir,
commands: Vec<Vec<String>>,
env: HashMap<String, String>,
) -> thread::JoinHandle<()> {
let reader = Self { handle, state, file_handle, reader };
thread::spawn(|| reader.run())
let reader = Self { state, script_directory };
thread::spawn(|| reader.run(commands, env))
}
fn run(mut self) {
let _ = Self::process_output(self.state.clone(), self.reader);
let success = match self.handle.wait() {
Ok(code) => code.success(),
_ => false,
};
let status = match success {
fn run(self, commands: Vec<Vec<String>>, env: HashMap<String, String>) {
let mut last_result = true;
for command in commands {
last_result = self.run_command(command, &env);
if !last_result {
break;
}
}
let status = match last_result {
true => ProcessStatus::Success,
false => ProcessStatus::Failure,
};
self.state.lock().unwrap().status = status;
}
fn run_command(&self, command: Vec<String>, env: &HashMap<String, String>) -> bool {
let (mut child, reader) = match self.launch_process(command, env) {
Ok(inner) => inner,
Err(e) => {
let mut state = self.state.lock().unwrap();
state.status = ProcessStatus::Failure;
state.output.push(e.to_string());
return false;
}
};
let _ = Self::process_output(self.state.clone(), reader);
match child.wait() {
Ok(code) => code.success(),
_ => false,
}
}
fn launch_process(
&self,
commands: Vec<String>,
env: &HashMap<String, String>,
) -> Result<(Child, PipeReader), CodeExecuteError> {
let (reader, writer) = os_pipe::pipe().map_err(CodeExecuteError::Pipe)?;
let writer_clone = writer.try_clone().map_err(CodeExecuteError::Pipe)?;
let (command, args) = commands.split_first().expect("no commands");
let child = process::Command::new(command)
.args(args)
.envs(env)
.current_dir(self.script_directory.path())
.stdin(Stdio::null())
.stdout(writer)
.stderr(writer_clone)
.spawn()
.map_err(|e| CodeExecuteError::SpawnProcess(command.clone(), e))?;
Ok((child, reader))
}
fn process_output(state: Arc<Mutex<ExecutionState>>, reader: os_pipe::PipeReader) -> io::Result<()> {
let reader = BufReader::new(reader);
for line in reader.lines() {
@ -231,8 +230,6 @@ impl ProcessStatus {
#[cfg(test)]
mod test {
use tempfile::tempdir;
use super::*;
use crate::markdown::elements::CodeAttributes;
@ -247,7 +244,7 @@ echo 'bye'"
language: CodeLanguage::Shell("sh".into()),
attributes: CodeAttributes { execute: true, ..Default::default() },
};
let handle = CodeExecutor::default().execute(&code).expect("execution failed");
let handle = SnippetExecutor::default().execute(&code).expect("execution failed");
let state = loop {
let state = handle.state();
if state.status.is_finished() {
@ -267,7 +264,7 @@ echo 'bye'"
language: CodeLanguage::Shell("sh".into()),
attributes: CodeAttributes { execute: false, ..Default::default() },
};
let result = CodeExecutor::default().execute(&code);
let result = SnippetExecutor::default().execute(&code);
assert!(result.is_err());
}
@ -283,7 +280,7 @@ echo 'hello world'
language: CodeLanguage::Shell("sh".into()),
attributes: CodeAttributes { execute: true, ..Default::default() },
};
let handle = CodeExecutor::default().execute(&code).expect("execution failed");
let handle = SnippetExecutor::default().execute(&code).expect("execution failed");
let state = loop {
let state = handle.state();
if state.status.is_finished() {
@ -308,7 +305,7 @@ echo 'hello world'
language: CodeLanguage::Shell("sh".into()),
attributes: CodeAttributes { execute: true, ..Default::default() },
};
let handle = CodeExecutor::default().execute(&code).expect("execution failed");
let handle = SnippetExecutor::default().execute(&code).expect("execution failed");
let state = loop {
let state = handle.state();
if state.status.is_finished() {
@ -322,28 +319,7 @@ echo 'hello world'
}
#[test]
fn custom_executor() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("rust.sh"), "hi").expect("writing script failed");
let executor = CodeExecutor::load(dir.path()).expect("load filed");
let script = executor.custom_executors.get(&CodeLanguage::Rust).expect("rust not found");
assert_eq!(script, b"hi");
}
#[test]
fn unknown_executor_language() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("potato.sh"), "").expect("writing script failed");
let executor = CodeExecutor::load(dir.path());
assert!(matches!(executor, Err(LoadExecutorsError::InvalidExecutor(_, _))));
}
#[test]
fn invalid_executor_extension() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("rust.potato"), "").expect("writing script failed");
let executor = CodeExecutor::load(dir.path());
assert!(matches!(executor, Err(LoadExecutorsError::InvalidExecutor(_, _))));
fn built_in_executors() {
SnippetExecutor::new(Default::default()).expect("invalid default executors");
}
}

View File

@ -1,6 +1,6 @@
use crate::{
custom::KeyBindingsConfig,
execute::CodeExecutor,
execute::SnippetExecutor,
markdown::parse::ParseError,
media::{
image::{Image, ImageSource},
@ -30,7 +30,7 @@ pub struct Exporter<'a> {
default_theme: &'a PresentationTheme,
resources: Resources,
third_party: ThirdPartyRender,
code_executor: Rc<CodeExecutor>,
code_executor: Rc<SnippetExecutor>,
themes: Themes,
options: PresentationBuilderOptions,
}
@ -42,7 +42,7 @@ impl<'a> Exporter<'a> {
default_theme: &'a PresentationTheme,
resources: Resources,
third_party: ThirdPartyRender,
code_executor: Rc<CodeExecutor>,
code_executor: Rc<SnippetExecutor>,
themes: Themes,
options: PresentationBuilderOptions,
) -> Self {

View File

@ -23,7 +23,7 @@ pub(crate) mod tools;
pub use crate::{
custom::{Config, ImageProtocol, ValidateOverflows},
demo::ThemesDemo,
execute::CodeExecutor,
execute::SnippetExecutor,
export::{ExportError, Exporter},
input::source::CommandSource,
markdown::parse::MarkdownParser,

View File

@ -2,9 +2,10 @@ use clap::{error::ErrorKind, CommandFactory, Parser};
use comrak::Arena;
use directories::ProjectDirs;
use presenterm::{
CodeExecutor, CommandSource, Config, Exporter, GraphicsMode, HighlightThemeSet, ImagePrinter, ImageProtocol,
ImageRegistry, MarkdownParser, PresentMode, PresentationBuilderOptions, PresentationTheme, PresentationThemeSet,
Presenter, PresenterOptions, Resources, Themes, ThemesDemo, ThirdPartyConfigs, ThirdPartyRender, ValidateOverflows,
CommandSource, Config, Exporter, GraphicsMode, HighlightThemeSet, ImagePrinter, ImageProtocol, ImageRegistry,
MarkdownParser, PresentMode, PresentationBuilderOptions, PresentationTheme, PresentationThemeSet, Presenter,
PresenterOptions, Resources, SnippetExecutor, Themes, ThemesDemo, ThirdPartyConfigs, ThirdPartyRender,
ValidateOverflows,
};
use std::{
env, io,
@ -91,7 +92,7 @@ fn create_splash() -> String {
struct Customizations {
config: Config,
themes: Themes,
code_executor: CodeExecutor,
code_executor: SnippetExecutor,
}
fn load_customizations(config_file_path: Option<PathBuf>) -> Result<Customizations, Box<dyn std::error::Error>> {
@ -107,7 +108,7 @@ fn load_customizations(config_file_path: Option<PathBuf>) -> Result<Customizatio
let themes = load_themes(&configs_path)?;
let config_file_path = config_file_path.unwrap_or_else(|| configs_path.join("config.yaml"));
let config = Config::load(&config_file_path)?;
let code_executor = CodeExecutor::load(&configs_path.join("executors"))?;
let code_executor = SnippetExecutor::new(config.snippet.exec.custom.clone())?;
Ok(Customizations { config, themes, code_executor })
}

View File

@ -1,4 +1,5 @@
use crate::style::TextStyle;
use serde_with::DeserializeFromStr;
use std::{convert::Infallible, fmt::Write, iter, ops::Range, path::PathBuf, str::FromStr};
use strum::EnumIter;
use unicode_width::UnicodeWidthStr;
@ -205,7 +206,7 @@ impl Code {
}
/// The language of a piece of code.
#[derive(Clone, Debug, PartialEq, Eq, EnumIter, PartialOrd, Ord)]
#[derive(Clone, Debug, PartialEq, Eq, EnumIter, PartialOrd, Ord, DeserializeFromStr)]
pub enum CodeLanguage {
Ada,
Asp,

View File

@ -1,7 +1,7 @@
use crate::{
custom::KeyBindingsConfig,
diff::PresentationDiffer,
execute::CodeExecutor,
execute::SnippetExecutor,
export::ImageReplacer,
input::source::{Command, CommandSource},
markdown::parse::{MarkdownParser, ParseError},
@ -46,7 +46,7 @@ pub struct Presenter<'a> {
parser: MarkdownParser<'a>,
resources: Resources,
third_party: ThirdPartyRender,
code_executor: Rc<CodeExecutor>,
code_executor: Rc<SnippetExecutor>,
state: PresenterState,
slides_with_pending_async_renders: HashSet<usize>,
image_printer: Arc<ImagePrinter>,
@ -63,7 +63,7 @@ impl<'a> Presenter<'a> {
parser: MarkdownParser<'a>,
resources: Resources,
third_party: ThirdPartyRender,
code_executor: Rc<CodeExecutor>,
code_executor: Rc<SnippetExecutor>,
themes: Themes,
image_printer: Arc<ImagePrinter>,
options: PresenterOptions,

View File

@ -1,7 +1,7 @@
use super::{execution::SnippetExecutionDisabledOperation, modals::KeyBindingsModalBuilder};
use crate::{
custom::{KeyBindingsConfig, OptionsConfig},
execute::CodeExecutor,
execute::SnippetExecutor,
markdown::{
elements::{
Code, CodeLanguage, Highlight, HighlightGroup, ListItem, ListItemType, MarkdownElement, ParagraphElement,
@ -96,7 +96,7 @@ pub(crate) struct PresentationBuilder<'a> {
chunk_mutators: Vec<Box<dyn ChunkMutator>>,
slides: Vec<Slide>,
highlighter: CodeHighlighter,
code_executor: Rc<CodeExecutor>,
code_executor: Rc<SnippetExecutor>,
theme: Cow<'a, PresentationTheme>,
resources: &'a mut Resources,
third_party: &'a mut ThirdPartyRender,
@ -117,7 +117,7 @@ impl<'a> PresentationBuilder<'a> {
default_theme: &'a PresentationTheme,
resources: &'a mut Resources,
third_party: &'a mut ThirdPartyRender,
code_executor: Rc<CodeExecutor>,
code_executor: Rc<SnippetExecutor>,
themes: &'a Themes,
image_registry: ImageRegistry,
bindings_config: KeyBindingsConfig,
@ -1081,7 +1081,7 @@ mod test {
let theme = PresentationTheme::default();
let mut resources = Resources::new("/tmp", Default::default());
let mut third_party = ThirdPartyRender::default();
let code_executor = Rc::new(CodeExecutor::default());
let code_executor = Rc::new(SnippetExecutor::default());
let themes = Themes::default();
let bindings = KeyBindingsConfig::default();
let builder = PresentationBuilder::new(

View File

@ -1,6 +1,6 @@
use super::separator::RenderSeparator;
use crate::{
execute::{CodeExecutor, ExecutionHandle, ExecutionState, ProcessStatus},
execute::{ExecutionHandle, ExecutionState, ProcessStatus, SnippetExecutor},
markdown::elements::{Code, Text, TextBlock},
presentation::{AsRenderOperations, PreformattedLine, RenderAsync, RenderAsyncState, RenderOperation},
render::properties::WindowSize,
@ -12,7 +12,7 @@ use std::{cell::RefCell, mem, rc::Rc};
use unicode_width::UnicodeWidthStr;
#[derive(Debug)]
struct RunCodeOperationInner {
struct RunSnippetOperationInner {
handle: Option<ExecutionHandle>,
output_lines: Vec<String>,
state: RenderAsyncState,
@ -21,24 +21,24 @@ struct RunCodeOperationInner {
#[derive(Debug)]
pub(crate) struct RunSnippetOperation {
code: Code,
executor: Rc<CodeExecutor>,
executor: Rc<SnippetExecutor>,
default_colors: Colors,
block_colors: Colors,
status_colors: ExecutionStatusBlockStyle,
inner: Rc<RefCell<RunCodeOperationInner>>,
inner: Rc<RefCell<RunSnippetOperationInner>>,
state_description: RefCell<Text>,
}
impl RunSnippetOperation {
pub(crate) fn new(
code: Code,
executor: Rc<CodeExecutor>,
executor: Rc<SnippetExecutor>,
default_colors: Colors,
block_colors: Colors,
status_colors: ExecutionStatusBlockStyle,
) -> Self {
let inner =
RunCodeOperationInner { handle: None, output_lines: Vec::new(), state: RenderAsyncState::default() };
RunSnippetOperationInner { handle: None, output_lines: Vec::new(), state: RenderAsyncState::default() };
let running_colors = status_colors.running.clone();
Self {
code,