1
0
mirror of https://github.com/mfontanini/presenterm.git synced 2026-09-01 23:33:21 -06:00

feat: allow passing in env file to snippet

This commit is contained in:
Matias Fontanini 2026-04-25 15:51:28 -07:00
parent 874cea8c70
commit 894ce397da
6 changed files with 93 additions and 12 deletions

View File

@ -66,6 +66,7 @@ impl SnippetExecutor {
&self,
language: &SnippetLanguage,
spec: &SnippetExecutorSpec,
custom_env: HashMap<String, String>,
) -> Result<LanguageSnippetExecutor, UnsupportedExecution> {
let language_config = self
.executors
@ -79,10 +80,14 @@ impl SnippetExecutor {
})?
}
};
let mut env = config.environment.clone();
env.extend(custom_env);
Ok(LanguageSnippetExecutor {
hidden_line_prefix: language_config.hidden_line_prefix.clone(),
config,
cwd: self.cwd.clone(),
env,
})
}
@ -126,6 +131,7 @@ pub(crate) struct LanguageSnippetExecutor {
hidden_line_prefix: Option<String>,
config: SnippetExecutorConfig,
cwd: PathBuf,
env: HashMap<String, String>,
}
impl LanguageSnippetExecutor {
@ -141,7 +147,7 @@ impl LanguageSnippetExecutor {
state.clone(),
script_dir,
self.config.commands.clone(),
self.config.environment.clone(),
self.env.clone(),
self.cwd.clone(),
output_type,
);
@ -177,7 +183,7 @@ impl LanguageSnippetExecutor {
let mut command = portable_pty::CommandBuilder::new(command);
command.args(args);
command.cwd(&self.cwd);
for (key, value) in &self.config.environment {
for (key, value) in &self.env {
command.env(key, value);
}
Ok(PtySnippetContext { command, _temp: script_dir })
@ -190,7 +196,7 @@ impl LanguageSnippetExecutor {
let (command, args) = commands.split_first().expect("no commands");
let child = process::Command::new(command)
.args(args)
.envs(&self.config.environment)
.envs(&self.env)
.current_dir(&self.cwd)
.stderr(Stdio::piped())
.spawn()
@ -419,7 +425,9 @@ echo 'bye'"
..Default::default()
},
};
let executor = SnippetExecutor::default().language_executor(&snippet.language, &Default::default()).unwrap();
let executor = SnippetExecutor::default()
.language_executor(&snippet.language, &Default::default(), Default::default())
.unwrap();
let handle = executor.execute_async(&snippet).expect("execution failed");
let state = loop {
let state = handle.state.lock().unwrap();
@ -447,7 +455,9 @@ echo 'hello world'
..Default::default()
},
};
let executor = SnippetExecutor::default().language_executor(&snippet.language, &Default::default()).unwrap();
let executor = SnippetExecutor::default()
.language_executor(&snippet.language, &Default::default(), Default::default())
.unwrap();
let handle = executor.execute_async(&snippet).expect("execution failed");
let state = loop {
let state = handle.state.lock().unwrap();
@ -476,7 +486,9 @@ echo 'hello world'
..Default::default()
},
};
let executor = SnippetExecutor::default().language_executor(&snippet.language, &Default::default()).unwrap();
let executor = SnippetExecutor::default()
.language_executor(&snippet.language, &Default::default(), Default::default())
.unwrap();
let handle = executor.execute_async(&snippet).expect("execution failed");
let state = loop {
let state = handle.state.lock().unwrap();

View File

@ -289,6 +289,7 @@ impl SnippetParser {
HighlightedLines(lines) => attributes.highlight_groups = lines,
Width(width) => attributes.width = Some(width),
ExpectedExecutionResult(result) => attributes.expected_execution_result = result,
Env(path) => attributes.env_file = Some(path),
};
processed_attributes.push(discriminant);
input = rest;
@ -352,6 +353,7 @@ impl SnippetParser {
}
},
"pty" => SnippetAttribute::ExecPty(SnippetExecutorSpec::default(), parameter.parse()?),
"env" => SnippetAttribute::Env(parameter.to_string().into()),
_ => return Err(SnippetBlockParseError::InvalidToken(Self::next_identifier(input).into())),
}
}
@ -471,6 +473,7 @@ enum SnippetAttribute {
AcquireTerminal(SnippetExecutorSpec),
ExpectedExecutionResult(ExpectedSnippetExecutionResult),
Id(String),
Env(PathBuf),
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
@ -717,6 +720,9 @@ pub(crate) struct SnippetAttributes {
/// The identifier for a snippet.
pub(crate) id: Option<String>,
/// The environment file to load.
pub(crate) env_file: Option<PathBuf>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]

View File

@ -122,6 +122,9 @@ pub(crate) enum InvalidPresentation {
#[error("snippet id '{0}' already exists")]
SnippetAlreadyExists(String),
#[error("invalid env file format: expected <key>=<value>")]
InvalidEnvFile,
}
#[derive(Clone, Debug)]

View File

@ -25,7 +25,7 @@ use crate::{
},
};
use itertools::Itertools;
use std::{cell::RefCell, rc::Rc};
use std::{cell::RefCell, collections::HashMap, path::Path, rc::Rc};
impl PresentationBuilder<'_, '_> {
pub(crate) fn push_code(&mut self, info: String, code: String, source_position: SourcePosition) -> BuildResult {
@ -61,7 +61,11 @@ impl PresentationBuilder<'_, '_> {
(SnippetExecutorSpec::Default, SnippetExecution::Exec(args)) => &args.spec,
_ => spec,
};
let executor = self.snippet_executor.language_executor(&snippet.language, spec)?;
let env = match &snippet.attributes.env_file {
Some(path) => self.load_env(path, source_position)?,
None => HashMap::new(),
};
let executor = self.snippet_executor.language_executor(&snippet.language, spec, env)?;
self.push_validator(&snippet, &executor);
}
@ -85,7 +89,11 @@ impl PresentationBuilder<'_, '_> {
self.push_execution_disabled_operation(exec_type);
}
SnippetExecution::Exec(args) => {
let executor = self.snippet_executor.language_executor(&snippet.language, &args.spec)?;
let env = match &snippet.attributes.env_file {
Some(path) => self.load_env(path, source_position)?,
None => HashMap::new(),
};
let executor = self.snippet_executor.language_executor(&snippet.language, &args.spec, env)?;
match args.repr {
SnippetRepr::Image => {
self.push_code_as_image(snippet, executor)?;
@ -404,6 +412,31 @@ impl PresentationBuilder<'_, '_> {
let operation = ValidateSnippetOperation::new(snippet.clone(), executor.clone());
self.chunk_operations.push(RenderOperation::RenderAsync(Rc::new(operation)));
}
fn load_env(&self, path: &Path, source_position: SourcePosition) -> Result<HashMap<String, String>, BuildError> {
let base = self.resource_base_path();
let contents = self.resources.external_text_file(path, &base).map_err(|e| {
self.invalid_presentation(
source_position,
InvalidPresentation::IncludeMarkdown { path: path.to_path_buf(), error: e },
)
})?;
let mut vars = HashMap::new();
for line in contents.lines() {
if line.starts_with('#') {
continue;
}
let line = line.trim();
if line.is_empty() {
continue;
}
let Some((key, value)) = line.split_once('=') else {
return Err(self.invalid_presentation(source_position, InvalidPresentation::InvalidEnvFile));
};
vars.insert(key.to_string(), value.to_string());
}
Ok(vars)
}
}
#[derive(Debug)]
@ -889,4 +922,25 @@ echo "{qr}"
let expected: Vec<_> = [empty.as_str()].into_iter().chain(qr.lines()).chain([empty.as_str()]).collect();
assert_eq!(lines, expected);
}
#[test]
fn env_file() {
let temp = tempfile::NamedTempFile::new().expect("failed to create tempfile");
let path = temp.path();
fs::write(path, "FOO=42\nBAR=1337\n\nTAR=hi\n# a comment\n").unwrap();
let path = path.to_string_lossy();
let input = format!(
"
```bash +exec_replace +env:{path}
echo \"$FOO\"
echo \"$BAR\"
echo \"$TAR\"
```
"
);
let lines = Test::new(input).render().rows(5).columns(5).into_lines();
let expected = &[" ", "42 ", "1337 ", "hi ", " "];
assert_eq!(lines, expected);
}
}

View File

@ -346,7 +346,9 @@ mod tests {
..Default::default()
},
};
let executor = SnippetExecutor::default().language_executor(&snippet.language, &Default::default()).unwrap();
let executor = SnippetExecutor::default()
.language_executor(&snippet.language, &Default::default(), Default::default())
.unwrap();
let policy = RenderAsyncStartPolicy::OnDemand;
let handle = SnippetHandle::new(snippet, executor, policy);
RunSnippetTrigger::new(handle)

View File

@ -127,7 +127,9 @@ mod tests {
language: SnippetLanguage::Rust,
attributes: SnippetAttributes { expected_execution_result, ..Default::default() },
};
let executor = SnippetExecutor::default().language_executor(&snippet.language, &Default::default()).unwrap();
let executor = SnippetExecutor::default()
.language_executor(&snippet.language, &Default::default(), Default::default())
.unwrap();
let state = Arc::new(Mutex::new(State::default()));
let mut pollable =
OperationPollable { snippet: snippet.clone(), executor: executor.clone(), state: state.clone() };
@ -154,7 +156,9 @@ mod tests {
language: SnippetLanguage::Rust,
attributes: SnippetAttributes { expected_execution_result, ..Default::default() },
};
let executor = SnippetExecutor::default().language_executor(&snippet.language, &Default::default()).unwrap();
let executor = SnippetExecutor::default()
.language_executor(&snippet.language, &Default::default(), Default::default())
.unwrap();
let state = Arc::new(Mutex::new(State::default()));
let mut pollable =
OperationPollable { snippet: snippet.clone(), executor: executor.clone(), state: state.clone() };