use crate::{
code::{
execute::{ExecutionHandle, ExecutionState, LanguageSnippetExecutor, ProcessStatus},
snippet::Snippet,
},
markdown::{
elements::{Line, Text},
text_style::{Colors, TextStyle},
},
render::{
operation::{
AsRenderOperations, BlockLine, Pollable, PollableState, RenderAsync, RenderAsyncStartPolicy,
RenderOperation,
},
properties::WindowSize,
},
terminal::ansi::AnsiParser,
theme::{Alignment, ExecutionOutputBlockStyle, ExecutionStatusBlockStyle},
ui::{
execution::pty::{PtySnippetHandle, RunPtySnippetTrigger},
separator::{RenderSeparator, SeparatorWidth},
},
};
use std::{
io::BufRead,
iter,
rc::Rc,
sync::{Arc, Mutex},
};
const MINIMUM_SEPARATOR_WIDTH: u16 = 32;
#[derive(Default, Debug)]
enum State {
#[default]
Initial,
Running(ExecutionHandle),
Done,
}
#[derive(Debug)]
struct Inner {
snippet: Snippet,
executor: LanguageSnippetExecutor,
output_lines: Vec<Line>,
max_line_length: u16,
process_status: Option<ProcessStatus>,
state: State,
policy: RenderAsyncStartPolicy,
}
#[derive(Debug)]
pub(crate) struct SnippetOutputOperation {
default_colors: Colors,
style: ExecutionOutputBlockStyle,
block_length: u16,
alignment: Alignment,
handle: SnippetHandle,
font_size: u8,
}
impl SnippetOutputOperation {
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
handle: SnippetHandle,
default_colors: Colors,
style: ExecutionOutputBlockStyle,
block_length: u16,
alignment: Alignment,
font_size: u8,
) -> Self {
let block_length = alignment.adjust_size(block_length);
Self { default_colors, style, block_length, alignment, handle, font_size }
}
}
impl AsRenderOperations for SnippetOutputOperation {
fn as_render_operations(&self, _dimensions: &WindowSize) -> Vec<RenderOperation> {
let inner = self.handle.0.lock().unwrap();
if let State::Initial = inner.state {
return Vec::new();
}
let mut operations = vec![];
let block_colors = self.style.style.colors;
if block_colors.background.is_some() {
operations.push(RenderOperation::SetColors(block_colors));
}
if !inner.output_lines.is_empty() {
let has_margin = match &self.alignment {
Alignment::Left { margin } => !margin.is_empty(),
Alignment::Right { margin } => !margin.is_empty(),
Alignment::Center { minimum_margin, minimum_size } => !minimum_margin.is_empty() || minimum_size != &0,
};
let padding = self.style.padding;
let block_length =
if has_margin { self.block_length.max(inner.max_line_length) } else { inner.max_line_length };
let vertical_padding = iter::repeat_n(" ", padding.vertical as usize).map(Line::from);
let lines = vertical_padding.clone().chain(inner.output_lines.iter().cloned()).chain(vertical_padding);
let style = TextStyle::default().size(self.font_size);
for mut line in lines {
line.apply_style(&style);
let prefix = Text::new(" ".repeat(padding.horizontal as usize), style).into();
operations.push(RenderOperation::RenderBlockLine(BlockLine {
prefix,
right_padding_length: padding.horizontal as u16,
repeat_prefix_on_wrap: false,
text: line.into(),
block_length,
alignment: self.alignment,
block_color: block_colors.background,
}));
operations.push(RenderOperation::RenderLineBreak);
}
}
operations.extend([RenderOperation::SetColors(self.default_colors)]);
operations
}
}
struct OperationPollable {
inner: Arc<Mutex<Inner>>,
last_length: usize,
}
impl OperationPollable {
fn try_start(&self, inner: &mut Inner) {
if !matches!(inner.state, State::Initial) {
return;
}
inner.state = match inner.executor.execute_async(&inner.snippet) {
Ok(handle) => State::Running(handle),
Err(e) => {
inner.output_lines = vec![e.to_string().into()];
State::Done
}
}
}
}
impl Pollable for OperationPollable {
fn poll(&mut self) -> PollableState {
let mut inner = self.inner.lock().unwrap();
self.try_start(&mut inner);
let State::Running(handle) = &mut inner.state else {
return PollableState::Done;
};
let mut state = handle.state.lock().unwrap();
let ExecutionState { output, status } = &mut *state;
let status = *status;
let modified = output.len() != self.last_length;
let mut lines = Vec::new();
for line in output.lines() {
let mut line = line.expect("invalid utf8");
if line.contains('\t') {
line = line.replace('\t', " ");
}
lines.push(line);
}
drop(state);
let mut max_line_length = 0;
let (lines, _) = AnsiParser::new(Default::default()).parse_lines(&lines);
for line in &lines {
let width = u16::try_from(line.width()).unwrap_or(u16::MAX);
max_line_length = max_line_length.max(width);
}
let is_finished = status.is_finished();
inner.process_status = Some(status);
inner.output_lines = lines;
inner.max_line_length = inner.max_line_length.max(max_line_length);
if is_finished {
inner.state = State::Done;
PollableState::Done
} else {
match modified {
true => PollableState::Modified,
false => PollableState::Unmodified,
}
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct SnippetHandle(Arc<Mutex<Inner>>);
impl SnippetHandle {
pub(crate) fn new(code: Snippet, executor: LanguageSnippetExecutor, policy: RenderAsyncStartPolicy) -> Self {
let inner = Inner {
snippet: code,
executor,
process_status: Default::default(),
output_lines: Default::default(),
max_line_length: Default::default(),
state: Default::default(),
policy,
};
Self(Arc::new(Mutex::new(inner)))
}
pub(crate) fn snippet(&self) -> Snippet {
self.0.lock().unwrap().snippet.clone()
}
}
#[derive(Debug)]
pub(crate) struct RunSnippetTrigger(Arc<Mutex<Inner>>);
impl RunSnippetTrigger {
pub(crate) fn new(handle: SnippetHandle) -> Self {
Self(handle.0)
}
}
impl AsRenderOperations for RunSnippetTrigger {
fn as_render_operations(&self, _dimensions: &WindowSize) -> Vec<RenderOperation> {
vec![]
}
}
impl RenderAsync for RunSnippetTrigger {
fn pollable(&self) -> Box<dyn Pollable> {
Box::new(OperationPollable { inner: self.0.clone(), last_length: 0 })
}
fn start_policy(&self) -> RenderAsyncStartPolicy {
self.0.lock().unwrap().policy
}
}
#[derive(Debug)]
pub(crate) struct ExecIndicatorStyle {
pub(crate) theme: ExecutionStatusBlockStyle,
pub(crate) block_length: u16,
pub(crate) font_size: u8,
pub(crate) alignment: Alignment,
}
#[derive(Clone, Debug)]
pub(crate) enum WrappedSnippetHandle {
Normal(SnippetHandle),
Pty(PtySnippetHandle),
}
impl WrappedSnippetHandle {
pub(crate) fn process_status(&self) -> Option<ProcessStatus> {
match self {
Self::Normal(handle) => handle.0.lock().unwrap().process_status,
Self::Pty(handle) => handle.process_status(),
}
}
pub(crate) fn build_trigger(&self) -> Box<dyn RenderAsync> {
match self.clone() {
Self::Normal(handle) => Box::new(RunSnippetTrigger::new(handle)),
Self::Pty(handle) => Box::new(RunPtySnippetTrigger::new(handle)),
}
}
}
impl From<SnippetHandle> for WrappedSnippetHandle {
fn from(handle: SnippetHandle) -> Self {
Self::Normal(handle)
}
}
impl From<PtySnippetHandle> for WrappedSnippetHandle {
fn from(handle: PtySnippetHandle) -> Self {
Self::Pty(handle)
}
}
#[derive(Debug)]
pub(crate) struct ExecIndicator {
handle: WrappedSnippetHandle,
separator_width: SeparatorWidth,
theme: ExecutionStatusBlockStyle,
font_size: u8,
}
impl ExecIndicator {
pub(crate) fn new<T: Into<WrappedSnippetHandle>>(handle: T, style: ExecIndicatorStyle) -> Self {
let ExecIndicatorStyle { theme, block_length, font_size, alignment } = style;
let block_length = alignment.adjust_size(block_length);
let separator_width = match &alignment {
Alignment::Left { .. } | Alignment::Right { .. } => SeparatorWidth::FitToWindow,
Alignment::Center { .. } => {
SeparatorWidth::Fixed(block_length.max(MINIMUM_SEPARATOR_WIDTH * font_size as u16))
}
};
let handle = handle.into();
Self { handle, separator_width, theme, font_size }
}
}
impl AsRenderOperations for ExecIndicator {
fn as_render_operations(&self, _dimensions: &WindowSize) -> Vec<RenderOperation> {
let status = self.handle.process_status();
let description = match status {
Some(ProcessStatus::Running) => Text::new("running", self.theme.running_style),
Some(ProcessStatus::Success) => Text::new("finished", self.theme.success_style),
Some(ProcessStatus::Failure) => Text::new("finished with error", self.theme.failure_style),
None => Text::new("not started", self.theme.not_started_style),
};
let heading = Line(vec![" [".into(), description.clone(), "] ".into()]);
let separator = RenderSeparator::new(heading, self.separator_width, self.font_size);
vec![
RenderOperation::RenderLineBreak,
RenderOperation::RenderDynamic(Rc::new(separator)),
RenderOperation::RenderLineBreak,
]
}
}
#[cfg(all(target_os = "linux", test))]
mod tests {
use super::*;
use crate::{
code::{
execute::SnippetExecutor,
snippet::{SnippetAttributes, SnippetExecution, SnippetLanguage},
},
markdown::{
elements::{Line, Text},
text_style::Color,
},
};
fn make_run_shell(code: &str) -> RunSnippetTrigger {
let snippet = Snippet {
contents: code.into(),
language: SnippetLanguage::Bash,
attributes: SnippetAttributes {
execution: SnippetExecution::Exec(Default::default()),
..Default::default()
},
};
let executor = SnippetExecutor::default().language_executor(&snippet.language, &Default::default()).unwrap();
let policy = RenderAsyncStartPolicy::OnDemand;
let handle = SnippetHandle::new(snippet, executor, policy);
RunSnippetTrigger::new(handle)
}
#[test]
fn run_command() {
let handle = make_run_shell("echo -e '\\033[1;31mhi mom'");
let mut pollable = handle.pollable();
while let PollableState::Modified | PollableState::Unmodified = pollable.poll() {}
let inner = handle.0.lock().unwrap();
let line = Line::from(Text::new("hi mom", TextStyle::default().fg_color(Color::DarkRed).bold()));
assert_eq!(inner.output_lines, vec![line]);
}
#[test]
fn multiple_pollables() {
let handle = make_run_shell("echo -e '\\033[1;31mhi mom'");
let mut main_pollable = handle.pollable();
let mut pollable2 = handle.pollable();
while let PollableState::Modified | PollableState::Unmodified = main_pollable.poll() {}
assert_eq!(pollable2.poll(), PollableState::Done);
let mut pollable3 = handle.pollable();
assert_eq!(pollable3.poll(), PollableState::Done);
}
}