Rework jobs view & error handling improvements

Job status is now shown in the top menu bar,
with a new Jobs window that can be toggled.

Build and diff errors are now handled more
gracefully.

Fixes #40
This commit is contained in:
2024-09-28 12:13:46 -06:00
parent bb039a1445
commit c7b85518ab
5 changed files with 233 additions and 78 deletions

View File

@@ -1,58 +1,162 @@
use std::cmp::Ordering;
use egui::{ProgressBar, RichText, Widget};
use crate::{jobs::JobQueue, views::appearance::Appearance};
use crate::{
jobs::{JobQueue, JobStatus},
views::appearance::Appearance,
};
pub fn jobs_ui(ui: &mut egui::Ui, jobs: &mut JobQueue, appearance: &Appearance) {
ui.label("Jobs");
if ui.button("Clear").clicked() {
jobs.clear_errored();
}
let mut remove_job: Option<usize> = None;
let mut any_jobs = false;
for job in jobs.iter_mut() {
let Ok(status) = job.context.status.read() else {
continue;
};
ui.group(|ui| {
ui.horizontal(|ui| {
ui.label(&status.title);
if ui.small_button("").clicked() {
if job.handle.is_some() {
job.should_remove = true;
if let Err(e) = job.cancel.send(()) {
log::error!("Failed to cancel job: {e:?}");
}
} else {
remove_job = Some(job.id);
any_jobs = true;
ui.separator();
ui.horizontal(|ui| {
ui.label(&status.title);
if ui.small_button("").clicked() {
if job.handle.is_some() {
if let Err(e) = job.cancel.send(()) {
log::error!("Failed to cancel job: {e:?}");
}
}
});
let mut bar = ProgressBar::new(status.progress_percent);
if let Some(items) = &status.progress_items {
bar = bar.text(format!("{} / {}", items[0], items[1]));
}
bar.ui(ui);
const STATUS_LENGTH: usize = 80;
if let Some(err) = &status.error {
let err_string = format!("{:#}", err);
ui.colored_label(
appearance.delete_color,
if err_string.len() > STATUS_LENGTH - 10 {
format!("Error: {}", &err_string[0..STATUS_LENGTH - 10])
} else {
format!("Error: {:width$}", err_string, width = STATUS_LENGTH - 7)
},
)
.on_hover_text_at_pointer(RichText::new(err_string).color(appearance.delete_color));
} else {
ui.label(if status.status.len() > STATUS_LENGTH - 3 {
format!("{}", &status.status[0..STATUS_LENGTH - 3])
} else {
format!("{:width$}", &status.status, width = STATUS_LENGTH)
})
.on_hover_text_at_pointer(&status.status);
remove_job = Some(job.id);
}
}
});
let mut bar = ProgressBar::new(status.progress_percent);
if let Some(items) = &status.progress_items {
bar = bar.text(format!("{} / {}", items[0], items[1]));
}
bar.ui(ui);
const STATUS_LENGTH: usize = 80;
if let Some(err) = &status.error {
let err_string = format!("{:#}", err);
ui.colored_label(
appearance.delete_color,
if err_string.len() > STATUS_LENGTH - 10 {
format!("Error: {}", &err_string[0..STATUS_LENGTH - 10])
} else {
format!("Error: {:width$}", err_string, width = STATUS_LENGTH - 7)
},
)
.on_hover_text_at_pointer(RichText::new(&err_string).color(appearance.delete_color))
.context_menu(|ui| {
if ui.button("Copy full message").clicked() {
ui.output_mut(|o| o.copied_text = err_string);
}
});
} else {
ui.label(if status.status.len() > STATUS_LENGTH - 3 {
format!("{}", &status.status[0..STATUS_LENGTH - 3])
} else {
format!("{:width$}", &status.status, width = STATUS_LENGTH)
})
.on_hover_text_at_pointer(&status.status)
.context_menu(|ui| {
if ui.button("Copy full message").clicked() {
ui.output_mut(|o| o.copied_text = status.status.clone());
}
});
}
}
if !any_jobs {
ui.label("No jobs");
}
if let Some(idx) = remove_job {
jobs.remove(idx);
}
}
struct JobStatusDisplay {
title: String,
progress_items: Option<[u32; 2]>,
error: bool,
}
impl From<&JobStatus> for JobStatusDisplay {
fn from(status: &JobStatus) -> Self {
Self {
title: status.title.clone(),
progress_items: status.progress_items,
error: status.error.is_some(),
}
}
}
pub fn jobs_menu_ui(ui: &mut egui::Ui, jobs: &mut JobQueue, appearance: &Appearance) -> bool {
ui.label("Jobs:");
let mut statuses = Vec::new();
for job in jobs.iter_mut() {
let Ok(status) = job.context.status.read() else {
continue;
};
statuses.push(JobStatusDisplay::from(&*status));
}
let running_jobs = statuses.iter().filter(|s| !s.error).count();
let error_jobs = statuses.iter().filter(|s| s.error).count();
let mut clicked = false;
let spinner =
egui::Spinner::new().size(appearance.ui_font.size * 0.9).color(appearance.text_color);
match running_jobs.cmp(&1) {
Ordering::Equal => {
spinner.ui(ui);
let running_job = statuses.iter().find(|s| !s.error).unwrap();
let text = if let Some(items) = running_job.progress_items {
format!("{} ({}/{})", running_job.title, items[0], items[1])
} else {
running_job.title.clone()
};
clicked |= ui.link(RichText::new(text)).clicked();
}
Ordering::Greater => {
spinner.ui(ui);
clicked |= ui.link(format!("{} running", running_jobs)).clicked();
}
_ => (),
}
match error_jobs.cmp(&1) {
Ordering::Equal => {
let error_job = statuses.iter().find(|s| s.error).unwrap();
clicked |= ui
.link(
RichText::new(format!("{} error", error_job.title))
.color(appearance.delete_color),
)
.clicked();
}
Ordering::Greater => {
clicked |= ui
.link(
RichText::new(format!("{} errors", error_jobs)).color(appearance.delete_color),
)
.clicked();
}
_ => (),
}
if running_jobs == 0 && error_jobs == 0 {
clicked |= ui.link("None").clicked();
}
clicked
}
pub fn jobs_window(
ctx: &egui::Context,
show: &mut bool,
jobs: &mut JobQueue,
appearance: &Appearance,
) {
egui::Window::new("Jobs").open(show).show(ctx, |ui| {
jobs_ui(ui, jobs, appearance);
});
}

View File

@@ -452,7 +452,7 @@ fn symbol_list_ui(
fn build_log_ui(ui: &mut Ui, status: &BuildStatus, appearance: &Appearance) {
ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
ui.horizontal(|ui| {
if ui.button("Copy command").clicked() {
if !status.cmdline.is_empty() && ui.button("Copy command").clicked() {
ui.output_mut(|output| output.copied_text.clone_from(&status.cmdline));
}
if ui.button("Copy log").clicked() {
@@ -465,9 +465,15 @@ fn build_log_ui(ui: &mut Ui, status: &BuildStatus, appearance: &Appearance) {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
ui.label(&status.cmdline);
ui.colored_label(appearance.replace_color, &status.stdout);
ui.colored_label(appearance.delete_color, &status.stderr);
if !status.cmdline.is_empty() {
ui.label(&status.cmdline);
}
if !status.stdout.is_empty() {
ui.colored_label(appearance.replace_color, &status.stdout);
}
if !status.stderr.is_empty() {
ui.colored_label(appearance.delete_color, &status.stderr);
}
});
});
}