use crate::child_app::ChildApp;
use crate::error::ExecutionError;
use cansi::{CategorisedSlice, Color};
use eframe::egui::{vec2, Color32, Label, ProgressBar, Ui, Widget};
use linkify::{LinkFinder, LinkKind};
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
pub fn progress_bar(description: &str, value: f32) {
progress_bar_with_id(description, description, value)
}
pub fn progress_bar_with_id(id: impl Hash, description: &str, value: f32) {
let mut h = DefaultHasher::new();
id.hash(&mut h);
OutputType::ProgressBar(description.to_string(), value).send(h.finish());
}
#[derive(Debug)]
pub(crate) enum Output {
None,
Err(ExecutionError),
Output(ChildApp, Vec<(u64, OutputType)>),
}
impl Output {
pub fn new_with_child(child: ChildApp) -> Self {
Self::Output(child, vec![])
}
}
impl Widget for &mut Output {
fn ui(self, ui: &mut Ui) -> eframe::egui::Response {
match self {
Output::None => ui.vertical(|_| {}).response,
Output::Err(err) => ui.colored_label(Color32::RED, err.to_string()),
Output::Output(child, output) => {
let str = child.read();
let mut iter = str.split(MAGIC);
if let Some(text) = iter.next() {
if !text.is_empty() {
output.push((0, OutputType::Text(text.to_string())))
}
}
while let Some(id) = iter.next() {
if let Ok(id) = id.parse() {
if let Some(new) = OutputType::parse(&mut iter) {
if let Some((_, exists)) = output.iter_mut().find(|(i, _)| *i == id) {
*exists = new;
} else {
output.push((id, new));
}
}
}
if let Some(text) = iter.next() {
let text = &text[1..];
if !text.is_empty() {
output.push((0, OutputType::Text(text.to_string())))
}
}
}
ui.vertical(|ui| {
if ui.button("Copy output").clicked() {
ui.ctx().output().copied_text = output
.iter()
.map(|(_, o)| match o {
OutputType::Text(text) => text,
OutputType::ProgressBar(text, _) => text,
})
.flat_map(|text| cansi::categorise_text(text))
.map(|slice| slice.text)
.collect::<String>();
}
for (_, o) in output {
match o {
OutputType::Text(ref text) => format_output(ui, text),
OutputType::ProgressBar(ref mess, value) => {
ui.add(
ProgressBar::new(*value)
.text(&mess[..mess.len() - 1])
.animate(true),
);
}
}
}
})
.response
}
}
}
}
#[derive(Debug)]
pub(crate) enum OutputType {
Text(String),
ProgressBar(String, f32),
}
const MAGIC: char = '\u{5FFFE}';
fn send_message(data: &[&str]) {
for d in data {
print!("{}{}", MAGIC, d);
}
println!("{}", MAGIC);
}
impl OutputType {
const PROGRESS_BAR_STR: &'static str = "progress-bar";
pub fn send(self, id: u64) {
match self {
OutputType::Text(s) => print!("{}", s),
OutputType::ProgressBar(desc, value) => send_message(&[
&id.to_string(),
Self::PROGRESS_BAR_STR,
&desc.replace('\n', " "),
&value.to_string(),
]),
}
}
pub fn parse<'a>(iter: &mut impl Iterator<Item = &'a str>) -> Option<Self> {
match iter.next() {
Some(Self::PROGRESS_BAR_STR) => Some(Self::ProgressBar(
format!("{}\n", iter.next().unwrap_or_default()),
iter.next()
.map(|s| s.parse().ok())
.flatten()
.unwrap_or_default(),
)),
None => None,
_ => panic!(),
}
}
}
fn format_output(ui: &mut Ui, text: &str) {
let output = cansi::categorise_text(text);
let previous = ui.style().spacing.item_spacing;
ui.style_mut().spacing.item_spacing = vec2(0.0, 0.0);
ui.horizontal_wrapped(|ui| {
for CategorisedSlice {
text,
fg_colour,
bg_colour,
intensity,
italic,
underline,
strikethrough,
..
} in output
{
for span in LinkFinder::new().spans(text) {
match span.kind() {
Some(LinkKind::Url) => ui.hyperlink(span.as_str()),
Some(LinkKind::Email) => {
ui.hyperlink_to(span.as_str(), format!("mailto:{}", span.as_str()))
}
Some(_) | None => {
let mut label = Label::new(span.as_str());
label = label.text_color(ansi_color_to_egui(fg_colour));
if bg_colour != Color::Black {
label = label.background_color(ansi_color_to_egui(bg_colour));
}
if italic {
label = label.italics();
}
if underline {
label = label.underline();
}
if strikethrough {
label = label.strikethrough();
}
label = match intensity {
cansi::Intensity::Normal => label,
cansi::Intensity::Bold => label.strong(),
cansi::Intensity::Faint => label.weak(),
};
ui.add(label)
}
};
}
}
});
ui.style_mut().spacing.item_spacing = previous;
}
fn ansi_color_to_egui(color: Color) -> Color32 {
match color {
Color::Black => Color32::from_rgb(0, 0, 0),
Color::Red => Color32::from_rgb(205, 49, 49),
Color::Green => Color32::from_rgb(13, 188, 121),
Color::Yellow => Color32::from_rgb(229, 229, 16),
Color::Blue => Color32::from_rgb(36, 114, 200),
Color::Magenta => Color32::from_rgb(188, 63, 188),
Color::Cyan => Color32::from_rgb(17, 168, 205),
Color::White => Color32::from_rgb(229, 229, 229),
Color::BrightBlack => Color32::from_rgb(102, 102, 102),
Color::BrightRed => Color32::from_rgb(241, 76, 76),
Color::BrightGreen => Color32::from_rgb(35, 209, 139),
Color::BrightYellow => Color32::from_rgb(245, 245, 67),
Color::BrightBlue => Color32::from_rgb(59, 142, 234),
Color::BrightMagenta => Color32::from_rgb(214, 112, 214),
Color::BrightCyan => Color32::from_rgb(41, 184, 219),
Color::BrightWhite => Color32::from_rgb(229, 229, 229),
}
}