Skip to content

Commit d0c10f9

Browse files
committed
feat: add +image to code blocks to consume their output as an image
1 parent e4b2b38 commit d0c10f9

File tree

5 files changed

+243
-44
lines changed

5 files changed

+243
-44
lines changed

src/code/execute.rs

+52-24
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use std::{
1010
collections::{BTreeMap, HashMap},
1111
fmt::{self, Debug},
1212
fs::File,
13-
io::{self, BufRead, BufReader, Write},
13+
io::{self, BufRead, BufReader, Read, Write},
1414
path::{Path, PathBuf},
1515
process::{self, Child, Stdio},
1616
sync::{Arc, Mutex},
@@ -59,12 +59,17 @@ impl SnippetExecutor {
5959
let config = self.language_config(snippet)?;
6060
let script_dir = Self::write_snippet(snippet, config)?;
6161
let state: Arc<Mutex<ExecutionState>> = Default::default();
62+
let output_type = match snippet.attributes.image {
63+
true => OutputType::Binary,
64+
false => OutputType::Lines,
65+
};
6266
let reader_handle = CommandsRunner::spawn(
6367
state.clone(),
6468
script_dir,
6569
config.commands.clone(),
6670
config.environment.clone(),
6771
self.cwd.to_path_buf(),
72+
output_type,
6873
);
6974
let handle = ExecutionHandle { state, reader_handle };
7075
Ok(handle)
@@ -183,15 +188,16 @@ impl CommandsRunner {
183188
commands: Vec<Vec<String>>,
184189
env: HashMap<String, String>,
185190
cwd: PathBuf,
191+
output_type: OutputType,
186192
) -> thread::JoinHandle<()> {
187193
let reader = Self { state, script_directory };
188-
thread::spawn(|| reader.run(commands, env, cwd))
194+
thread::spawn(move || reader.run(commands, env, cwd, output_type))
189195
}
190196

191-
fn run(self, commands: Vec<Vec<String>>, env: HashMap<String, String>, cwd: PathBuf) {
197+
fn run(self, commands: Vec<Vec<String>>, env: HashMap<String, String>, cwd: PathBuf, output_type: OutputType) {
192198
let mut last_result = true;
193199
for command in commands {
194-
last_result = self.run_command(command, &env, &cwd);
200+
last_result = self.run_command(command, &env, &cwd, output_type);
195201
if !last_result {
196202
break;
197203
}
@@ -203,17 +209,23 @@ impl CommandsRunner {
203209
self.state.lock().unwrap().status = status;
204210
}
205211

206-
fn run_command(&self, command: Vec<String>, env: &HashMap<String, String>, cwd: &Path) -> bool {
212+
fn run_command(
213+
&self,
214+
command: Vec<String>,
215+
env: &HashMap<String, String>,
216+
cwd: &Path,
217+
output_type: OutputType,
218+
) -> bool {
207219
let (mut child, reader) = match self.launch_process(command, env, cwd) {
208220
Ok(inner) => inner,
209221
Err(e) => {
210222
let mut state = self.state.lock().unwrap();
211223
state.status = ProcessStatus::Failure;
212-
state.output.push(e.to_string());
224+
state.output.extend(e.to_string().into_bytes());
213225
return false;
214226
}
215227
};
216-
let _ = Self::process_output(self.state.clone(), reader);
228+
let _ = Self::process_output(self.state.clone(), reader, output_type);
217229

218230
match child.wait() {
219231
Ok(code) => code.success(),
@@ -246,24 +258,41 @@ impl CommandsRunner {
246258
Ok((child, reader))
247259
}
248260

249-
fn process_output(state: Arc<Mutex<ExecutionState>>, reader: os_pipe::PipeReader) -> io::Result<()> {
250-
let reader = BufReader::new(reader);
251-
for line in reader.lines() {
252-
let mut line = line?;
253-
if line.contains('\t') {
254-
line = line.replace('\t', " ");
261+
fn process_output(
262+
state: Arc<Mutex<ExecutionState>>,
263+
mut reader: os_pipe::PipeReader,
264+
output_type: OutputType,
265+
) -> io::Result<()> {
266+
match output_type {
267+
OutputType::Lines => {
268+
let reader = BufReader::new(reader);
269+
for line in reader.lines() {
270+
let mut state = state.lock().unwrap();
271+
state.output.extend(line?.into_bytes());
272+
state.output.push(b'\n');
273+
}
274+
Ok(())
275+
}
276+
OutputType::Binary => {
277+
let mut buffer = Vec::new();
278+
reader.read_to_end(&mut buffer)?;
279+
state.lock().unwrap().output.extend(buffer);
280+
Ok(())
255281
}
256-
// TODO: consider not locking per line...
257-
state.lock().unwrap().output.push(line);
258282
}
259-
Ok(())
260283
}
261284
}
262285

286+
#[derive(Clone, Copy)]
287+
enum OutputType {
288+
Lines,
289+
Binary,
290+
}
291+
263292
/// The state of the execution of a process.
264293
#[derive(Clone, Default, Debug)]
265294
pub(crate) struct ExecutionState {
266-
pub(crate) output: Vec<String>,
295+
pub(crate) output: Vec<u8>,
267296
pub(crate) status: ProcessStatus,
268297
}
269298

@@ -307,8 +336,8 @@ echo 'bye'"
307336
}
308337
};
309338

310-
let expected_lines = vec!["hello world", "bye"];
311-
assert_eq!(state.output, expected_lines);
339+
let expected = b"hello world\nbye\n";
340+
assert_eq!(state.output, expected);
312341
}
313342

314343
#[test]
@@ -343,8 +372,8 @@ echo 'hello world'
343372
}
344373
};
345374

346-
let expected_lines = vec!["This message redirects to stderr", "hello world"];
347-
assert_eq!(state.output, expected_lines);
375+
let expected = b"This message redirects to stderr\nhello world\n";
376+
assert_eq!(state.output, expected);
348377
}
349378

350379
#[test]
@@ -368,9 +397,8 @@ echo 'hello world'
368397
}
369398
};
370399

371-
let expected_lines =
372-
vec!["this line was hidden", "this line was hidden and contains another prefix /// ", "hello world"];
373-
assert_eq!(state.output, expected_lines);
400+
let expected = b"this line was hidden\nthis line was hidden and contains another prefix /// \nhello world\n";
401+
assert_eq!(state.output, expected);
374402
}
375403

376404
#[test]

src/code/snippet.rs

+10
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,10 @@ impl SnippetParser {
235235
SnippetAttribute::LineNumbers => attributes.line_numbers = true,
236236
SnippetAttribute::Exec => attributes.execute = true,
237237
SnippetAttribute::ExecReplace => attributes.execute_replace = true,
238+
SnippetAttribute::Image => {
239+
attributes.execute_replace = true;
240+
attributes.image = true;
241+
}
238242
SnippetAttribute::Render => attributes.render = true,
239243
SnippetAttribute::NoBackground => attributes.no_background = true,
240244
SnippetAttribute::AcquireTerminal => attributes.acquire_terminal = true,
@@ -259,6 +263,7 @@ impl SnippetParser {
259263
"line_numbers" => SnippetAttribute::LineNumbers,
260264
"exec" => SnippetAttribute::Exec,
261265
"exec_replace" => SnippetAttribute::ExecReplace,
266+
"image" => SnippetAttribute::Image,
262267
"render" => SnippetAttribute::Render,
263268
"no_background" => SnippetAttribute::NoBackground,
264269
"acquire_terminal" => SnippetAttribute::AcquireTerminal,
@@ -373,6 +378,7 @@ enum SnippetAttribute {
373378
LineNumbers,
374379
Exec,
375380
ExecReplace,
381+
Image,
376382
Render,
377383
HighlightedLines(Vec<HighlightGroup>),
378384
Width(Percent),
@@ -570,6 +576,10 @@ pub(crate) struct SnippetAttributes {
570576
/// of its execution.
571577
pub(crate) execute_replace: bool,
572578

579+
/// Whether the snippet should be executed and its output should be considered to be an image
580+
/// and replaced with it.
581+
pub(crate) image: bool,
582+
573583
/// Whether a snippet is marked to be rendered.
574584
///
575585
/// A rendered snippet is transformed during parsing, leading to some visual

src/presentation/builder.rs

+23-9
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ use crate::{
3636
third_party::{ThirdPartyRender, ThirdPartyRenderError, ThirdPartyRenderRequest},
3737
ui::{
3838
execution::{
39-
DisplaySeparator, RunAcquireTerminalSnippet, RunSnippetOperation, SnippetExecutionDisabledOperation,
39+
DisplaySeparator, RunAcquireTerminalSnippet, RunImageSnippet, RunSnippetOperation,
40+
SnippetExecutionDisabledOperation,
4041
},
4142
footer::{FooterContext, FooterGenerator},
4243
modals::{IndexBuilder, KeyBindingsModalBuilder},
@@ -975,18 +976,31 @@ impl<'a> PresentationBuilder<'a> {
975976
style
976977
}
977978

978-
fn push_code_execution(&mut self, code: Snippet, block_length: usize, mode: ExecutionMode) -> BuildResult {
979-
if !self.code_executor.is_execution_supported(&code.language) {
980-
return Err(BuildError::UnsupportedExecution(code.language));
979+
fn push_code_execution(&mut self, snippet: Snippet, block_length: usize, mode: ExecutionMode) -> BuildResult {
980+
if !self.code_executor.is_execution_supported(&snippet.language) {
981+
return Err(BuildError::UnsupportedExecution(snippet.language));
981982
}
982-
if code.attributes.acquire_terminal {
983+
if snippet.attributes.image {
984+
let operation = RunImageSnippet::new(
985+
snippet,
986+
self.code_executor.clone(),
987+
self.image_registry.clone(),
988+
self.theme.execution_output.status.clone(),
989+
);
990+
operation.start_render();
991+
992+
let operation = RenderOperation::RenderAsync(Rc::new(operation));
993+
self.chunk_operations.push(operation);
994+
return Ok(());
995+
}
996+
if snippet.attributes.acquire_terminal {
983997
let block_length = block_length as u16;
984998
let block_length = match self.theme.code.alignment.clone().unwrap_or_default() {
985999
Alignment::Left { .. } | Alignment::Right { .. } => block_length,
9861000
Alignment::Center { minimum_size, .. } => block_length.max(minimum_size),
9871001
};
9881002
let operation = RunAcquireTerminalSnippet::new(
989-
code,
1003+
snippet,
9901004
self.code_executor.clone(),
9911005
self.theme.execution_output.status.clone(),
9921006
block_length,
@@ -999,14 +1013,14 @@ impl<'a> PresentationBuilder<'a> {
9991013
ExecutionMode::AlongSnippet => DisplaySeparator::On,
10001014
ExecutionMode::ReplaceSnippet => DisplaySeparator::Off,
10011015
};
1002-
let alignment = self.code_style(&code).alignment.unwrap_or_default();
1016+
let alignment = self.code_style(&snippet).alignment.unwrap_or_default();
10031017
let default_colors = self.theme.default_style.colors;
10041018
let mut execution_output_style = self.theme.execution_output.clone();
1005-
if code.attributes.no_background {
1019+
if snippet.attributes.no_background {
10061020
execution_output_style.colors.background = None;
10071021
}
10081022
let operation = RunSnippetOperation::new(
1009-
code,
1023+
snippet,
10101024
self.code_executor.clone(),
10111025
default_colors,
10121026
execution_output_style,

src/terminal/image/printer.rs

+13-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use crate::{
1414
use image::{DynamicImage, ImageError};
1515
use std::{
1616
borrow::Cow,
17-
io,
17+
fmt, io,
1818
path::{Path, PathBuf},
1919
sync::Arc,
2020
};
@@ -162,6 +162,18 @@ impl PrintImage for ImagePrinter {
162162
#[derive(Clone, Default)]
163163
pub(crate) struct ImageRegistry(pub Arc<ImagePrinter>);
164164

165+
impl fmt::Debug for ImageRegistry {
166+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167+
let inner = match self.0.as_ref() {
168+
ImagePrinter::Kitty(_) => "Kitty",
169+
ImagePrinter::Iterm(_) => "Iterm",
170+
ImagePrinter::Ascii(_) => "Ascii",
171+
ImagePrinter::Null => "Null",
172+
};
173+
write!(f, "ImageRegistry<{inner}>")
174+
}
175+
}
176+
165177
impl ImageRegistry {
166178
pub(crate) fn register_image(&self, image: DynamicImage) -> Result<Image, RegisterImageError> {
167179
let resource = self.0.register(image)?;

0 commit comments

Comments
 (0)