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

feat: allow acquiring terminal when running snippets #366

Merged
merged 1 commit into from
Sep 12, 2024
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
99 changes: 65 additions & 34 deletions src/execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,45 +54,69 @@ impl SnippetExecutor {
self.executors.contains_key(language)
}

/// Execute a piece of code.
pub(crate) fn execute(&self, code: &Snippet) -> Result<ExecutionHandle, CodeExecuteError> {
if !code.attributes.execute && !code.attributes.execute_replace {
return Err(CodeExecuteError::NotExecutableCode);
/// Execute a piece of code asynchronously.
pub(crate) fn execute_async(&self, snippet: &Snippet) -> Result<ExecutionHandle, CodeExecuteError> {
let config = self.language_config(snippet)?;
let script_dir = Self::write_snippet(snippet, config)?;
let state: Arc<Mutex<ExecutionState>> = Default::default();
let reader_handle = CommandsRunner::spawn(
state.clone(),
script_dir,
config.commands.clone(),
config.environment.clone(),
self.cwd.to_path_buf(),
);
let handle = ExecutionHandle { state, reader_handle };
Ok(handle)
}

/// Executes a piece of code synchronously.
pub(crate) fn execute_sync(&self, snippet: &Snippet) -> Result<(), CodeExecuteError> {
let config = self.language_config(snippet)?;
let script_dir = Self::write_snippet(snippet, config)?;
let script_dir_path = script_dir.path().to_string_lossy();
for mut commands in config.commands.clone() {
for command in &mut commands {
*command = command.replace("$pwd", &script_dir_path);
}
let (command, args) = commands.split_first().expect("no commands");
let child = process::Command::new(command)
.args(args)
.envs(&config.environment)
.current_dir(&self.cwd)
.stderr(Stdio::piped())
.spawn()
.map_err(|e| CodeExecuteError::SpawnProcess(command.clone(), e))?;

let output = child.wait_with_output().map_err(CodeExecuteError::Waiting)?;
if !output.status.success() {
let error = String::from_utf8_lossy(&output.stderr).to_string();
return Err(CodeExecuteError::Running(error));
}
}
let Some(config) = self.executors.get(&code.language) else {
return Err(CodeExecuteError::UnsupportedExecution);
};
let hide_prefix = config.hidden_line_prefix.as_deref();
Self::execute_lang(config, code.executable_contents(hide_prefix).as_bytes(), &self.cwd)
Ok(())
}

pub(crate) fn hidden_line_prefix(&self, language: &SnippetLanguage) -> Option<&str> {
self.executors.get(language).and_then(|lang| lang.hidden_line_prefix.as_deref())
}

fn execute_lang(
config: &LanguageSnippetExecutionConfig,
code: &[u8],
cwd: &Path,
) -> Result<ExecutionHandle, CodeExecuteError> {
fn language_config(&self, snippet: &Snippet) -> Result<&LanguageSnippetExecutionConfig, CodeExecuteError> {
if !snippet.attributes.execute && !snippet.attributes.execute_replace {
return Err(CodeExecuteError::NotExecutableCode);
}
self.executors.get(&snippet.language).ok_or(CodeExecuteError::UnsupportedExecution)
}

fn write_snippet(snippet: &Snippet, config: &LanguageSnippetExecutionConfig) -> Result<TempDir, CodeExecuteError> {
let hide_prefix = config.hidden_line_prefix.as_deref();
let code = snippet.executable_contents(hide_prefix);
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)?;
}

let state: Arc<Mutex<ExecutionState>> = Default::default();
let reader_handle = CommandsRunner::spawn(
state.clone(),
script_dir,
config.commands.clone(),
config.environment.clone(),
cwd.to_path_buf(),
);
let handle = ExecutionHandle { state, reader_handle };
Ok(handle)
let mut snippet_file = File::create(snippet_path).map_err(CodeExecuteError::TempDir)?;
snippet_file.write_all(code.as_bytes()).map_err(CodeExecuteError::TempDir)?;
Ok(script_dir)
}
}

Expand Down Expand Up @@ -124,6 +148,12 @@ pub(crate) enum CodeExecuteError {

#[error("error creating pipe: {0}")]
Pipe(io::Error),

#[error("error waiting for process to run: {0}")]
Waiting(io::Error),

#[error("error running process: {0}")]
Running(String),
}

/// A handle for the execution of a piece of code.
Expand Down Expand Up @@ -193,8 +223,9 @@ impl CommandsRunner {
) -> 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 script_dir = self.script_directory.path().to_string_lossy();
for command in &mut commands {
*command = command.replace("$pwd", &self.script_directory.path().to_string_lossy());
*command = command.replace("$pwd", &script_dir);
}
let (command, args) = commands.split_first().expect("no commands");
let child = process::Command::new(command)
Expand Down Expand Up @@ -262,7 +293,7 @@ echo 'bye'"
language: SnippetLanguage::Shell,
attributes: SnippetAttributes { execute: true, ..Default::default() },
};
let handle = SnippetExecutor::default().execute(&code).expect("execution failed");
let handle = SnippetExecutor::default().execute_async(&code).expect("execution failed");
let state = loop {
let state = handle.state.lock().unwrap();
if state.status.is_finished() {
Expand All @@ -282,7 +313,7 @@ echo 'bye'"
language: SnippetLanguage::Shell,
attributes: SnippetAttributes { execute: false, ..Default::default() },
};
let result = SnippetExecutor::default().execute(&code);
let result = SnippetExecutor::default().execute_async(&code);
assert!(result.is_err());
}

Expand All @@ -298,7 +329,7 @@ echo 'hello world'
language: SnippetLanguage::Shell,
attributes: SnippetAttributes { execute: true, ..Default::default() },
};
let handle = SnippetExecutor::default().execute(&code).expect("execution failed");
let handle = SnippetExecutor::default().execute_async(&code).expect("execution failed");
let state = loop {
let state = handle.state.lock().unwrap();
if state.status.is_finished() {
Expand All @@ -323,7 +354,7 @@ echo 'hello world'
language: SnippetLanguage::Shell,
attributes: SnippetAttributes { execute: true, ..Default::default() },
};
let handle = SnippetExecutor::default().execute(&code).expect("execution failed");
let handle = SnippetExecutor::default().execute_async(&code).expect("execution failed");
let state = loop {
let state = handle.state.lock().unwrap();
if state.status.is_finished() {
Expand Down
12 changes: 11 additions & 1 deletion src/processing/builder.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use super::{
code::{CodeBlockParser, CodeLine, ExternalFile, Highlight, HighlightGroup, Snippet, SnippetLanguage},
execution::{DisplaySeparator, SnippetExecutionDisabledOperation},
execution::{DisplaySeparator, RunAcquireTerminalCodeSnippet, SnippetExecutionDisabledOperation},
modals::KeyBindingsModalBuilder,
};
use crate::{
Expand Down Expand Up @@ -878,6 +878,16 @@ impl<'a> PresentationBuilder<'a> {
if !self.code_executor.is_execution_supported(&code.language) {
return Err(BuildError::UnsupportedExecution(code.language));
}
if code.attributes.acquire_terminal {
let operation = RunAcquireTerminalCodeSnippet::new(
code,
self.code_executor.clone(),
self.theme.execution_output.status.failure,
);
let operation = RenderOperation::RenderAsync(Rc::new(operation));
self.chunk_operations.push(operation);
return Ok(());
}
let separator = match mode {
ExecutionMode::AlongSnippet => DisplaySeparator::On,
ExecutionMode::ReplaceSnippet => DisplaySeparator::Off,
Expand Down
6 changes: 6 additions & 0 deletions src/processing/code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ impl CodeBlockParser {
Attribute::ExecReplace => attributes.execute_replace = true,
Attribute::AutoRender => attributes.auto_render = true,
Attribute::NoMargin => attributes.no_margin = true,
Attribute::AcquireTerminal => attributes.acquire_terminal = true,
Attribute::HighlightedLines(lines) => attributes.highlight_groups = lines,
Attribute::Width(width) => attributes.width = Some(width),
};
Expand All @@ -256,6 +257,7 @@ impl CodeBlockParser {
"exec_replace" => Attribute::ExecReplace,
"render" => Attribute::AutoRender,
"no_margin" => Attribute::NoMargin,
"acquire_terminal" => Attribute::AcquireTerminal,
token if token.starts_with("width:") => {
let value = input.split_once("+width:").unwrap().1;
let (width, input) = Self::parse_width(value)?;
Expand Down Expand Up @@ -371,6 +373,7 @@ enum Attribute {
HighlightedLines(Vec<HighlightGroup>),
Width(Percent),
NoMargin,
AcquireTerminal,
}

/// A code snippet.
Expand Down Expand Up @@ -574,6 +577,9 @@ pub(crate) struct SnippetAttributes {

/// Whether to add no margin to a snippet.
pub(crate) no_margin: bool,

/// Whether this code snippet acquires the terminal when ran.
pub(crate) acquire_terminal: bool,
}

#[derive(Clone, Debug, Default, PartialEq, Eq)]
Expand Down
94 changes: 90 additions & 4 deletions src/processing/execution.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
use crossterm::{
cursor,
terminal::{self, disable_raw_mode, enable_raw_mode},
ExecutableCommand,
};

use super::separator::{RenderSeparator, SeparatorWidth};
use crate::{
ansi::AnsiSplitter,
Expand All @@ -8,12 +14,18 @@ use crate::{
},
presentation::{AsRenderOperations, BlockLine, RenderAsync, RenderAsyncState, RenderOperation},
processing::code::Snippet,
render::properties::WindowSize,
render::{properties::WindowSize, terminal::should_hide_cursor},
style::{Colors, TextStyle},
theme::{Alignment, ExecutionStatusBlockStyle},
theme::{Alignment, ExecutionStatusBlockStyle, Margin},
PresentationTheme,
};
use std::{cell::RefCell, mem, rc::Rc};
use std::{
cell::RefCell,
io::{self},
mem,
ops::Deref,
rc::Rc,
};

#[derive(Debug)]
struct RunSnippetOperationInner {
Expand Down Expand Up @@ -176,7 +188,7 @@ impl RenderAsync for RunSnippetOperation {
if !matches!(inner.state, RenderAsyncState::NotStarted) {
return false;
}
match self.executor.execute(&self.code) {
match self.executor.execute_async(&self.code) {
Ok(handle) => {
inner.handle = Some(handle);
inner.state = RenderAsyncState::Rendering { modified: false };
Expand Down Expand Up @@ -230,3 +242,77 @@ impl RenderAsync for SnippetExecutionDisabledOperation {
RenderAsyncState::Rendered
}
}

#[derive(Clone, Debug)]
pub(crate) struct RunAcquireTerminalCodeSnippet {
snippet: Snippet,
executor: Rc<SnippetExecutor>,
error_message: RefCell<Option<Vec<String>>>,
error_colors: Colors,
}

impl RunAcquireTerminalCodeSnippet {
pub(crate) fn new(snippet: Snippet, executor: Rc<SnippetExecutor>, error_colors: Colors) -> Self {
Self { snippet, executor, error_message: Default::default(), error_colors }
}
}

impl RunAcquireTerminalCodeSnippet {
fn invoke(&self) -> Result<(), String> {
let mut stdout = io::stdout();
stdout
.execute(terminal::LeaveAlternateScreen)
.and_then(|_| disable_raw_mode())
.map_err(|e| format!("failed to deinit terminal: {e}"))?;

// save result for later, but first reinit the terminal
let result = self.executor.execute_sync(&self.snippet).map_err(|e| format!("failed to run snippet: {e}"));

stdout
.execute(terminal::EnterAlternateScreen)
.and_then(|_| enable_raw_mode())
.map_err(|e| format!("failed to reinit terminal: {e}"))?;
if should_hide_cursor() {
stdout.execute(cursor::Hide).map_err(|e| e.to_string())?;
}
result
}
}

impl AsRenderOperations for RunAcquireTerminalCodeSnippet {
fn as_render_operations(&self, _dimensions: &WindowSize) -> Vec<RenderOperation> {
let error_message = self.error_message.borrow();
match error_message.deref() {
Some(lines) => {
let mut ops = vec![RenderOperation::RenderLineBreak];
for line in lines {
ops.extend([
RenderOperation::RenderText {
line: vec![Text::new(line, TextStyle::default().colors(self.error_colors))].into(),
alignment: Alignment::Left { margin: Margin::Percent(25) },
},
RenderOperation::RenderLineBreak,
]);
}
ops
}
None => Vec::new(),
}
}
}

impl RenderAsync for RunAcquireTerminalCodeSnippet {
fn start_render(&self) -> bool {
if let Err(e) = self.invoke() {
let lines = e.lines().map(ToString::to_string).collect();
*self.error_message.borrow_mut() = Some(lines);
} else {
*self.error_message.borrow_mut() = None;
}
true
}

fn poll_state(&self) -> RenderAsyncState {
RenderAsyncState::Rendered
}
}
2 changes: 1 addition & 1 deletion src/render/terminal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ where
}
}

fn should_hide_cursor() -> bool {
pub(crate) fn should_hide_cursor() -> bool {
// WezTerm on Windows fails to display images if we've hidden the cursor so we **always** hide it
// unless we're on WezTerm on Windows.
let term = std::env::var("TERM_PROGRAM");
Expand Down
Loading