mirror of
https://github.com/mfontanini/presenterm.git
synced 2025-05-05 15:32:58 +00:00
feat: make snippet executors multiplatform
This commit is contained in:
parent
480f9475cc
commit
a2df29ddbe
31
build.rs
31
build.rs
@ -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(())
|
||||
}
|
||||
|
@ -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
73
executors.yaml
Normal 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"]
|
@ -1,5 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
temp=$(mktemp)
|
||||
g++ -std=c++20 -x c++ "$1" -o "$temp"
|
||||
"$temp"
|
@ -1,5 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
temp=$(mktemp)
|
||||
gcc -x c "$1" -o "$temp"
|
||||
"$temp"
|
@ -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"
|
@ -1,7 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
tempdir=$(mktemp -d)
|
||||
cd "$tempdir"
|
||||
cp "$1" Main.java
|
||||
java Main.java
|
||||
rm -rf "$tempdir"
|
@ -1,3 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
node "$1"
|
@ -1,7 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
tempdir=$(mktemp -d)
|
||||
cd "$tempdir"
|
||||
cp "$1" script.kts
|
||||
kotlinc -script script.kts
|
||||
rm -rf "$tempdir"
|
@ -1,3 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
lua "$1"
|
@ -1,3 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
nu "$1"
|
@ -1,3 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
perl "$1"
|
@ -1,3 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
exec python -u "$1"
|
@ -1,3 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
ruby "$1"
|
@ -1,3 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
rust-script "$1"
|
@ -1,5 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
temp=$(mktemp)
|
||||
rustc --crate-name "presenterm_snippet" "$1" -o "$temp"
|
||||
"$temp"
|
@ -27,7 +27,7 @@
|
||||
"src"
|
||||
"themes"
|
||||
"bat"
|
||||
"executors"
|
||||
"executors.yaml"
|
||||
];
|
||||
|
||||
buildSrc = flakeboxLib.filterSubPaths {
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
266
src/execute.rs
266
src/execute.rs
@ -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");
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
11
src/main.rs
11
src/main.rs
@ -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 })
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user