mirror of https://github.com/encounter/objdiff.git
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:
parent
bb039a1445
commit
c7b85518ab
|
@ -40,7 +40,7 @@ use crate::{
|
||||||
frame_history::FrameHistory,
|
frame_history::FrameHistory,
|
||||||
function_diff::function_diff_ui,
|
function_diff::function_diff_ui,
|
||||||
graphics::{graphics_window, GraphicsConfig, GraphicsViewState},
|
graphics::{graphics_window, GraphicsConfig, GraphicsViewState},
|
||||||
jobs::jobs_ui,
|
jobs::{jobs_menu_ui, jobs_window},
|
||||||
rlwinm::{rlwinm_decode_window, RlwinmDecodeViewState},
|
rlwinm::{rlwinm_decode_window, RlwinmDecodeViewState},
|
||||||
symbol_diff::{symbol_diff_ui, DiffViewState, View},
|
symbol_diff::{symbol_diff_ui, DiffViewState, View},
|
||||||
},
|
},
|
||||||
|
@ -62,6 +62,7 @@ pub struct ViewState {
|
||||||
pub show_arch_config: bool,
|
pub show_arch_config: bool,
|
||||||
pub show_debug: bool,
|
pub show_debug: bool,
|
||||||
pub show_graphics: bool,
|
pub show_graphics: bool,
|
||||||
|
pub show_jobs: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The configuration for a single object file.
|
/// The configuration for a single object file.
|
||||||
|
@ -483,6 +484,7 @@ impl eframe::App for App {
|
||||||
show_arch_config,
|
show_arch_config,
|
||||||
show_debug,
|
show_debug,
|
||||||
show_graphics,
|
show_graphics,
|
||||||
|
show_jobs,
|
||||||
} = view_state;
|
} = view_state;
|
||||||
|
|
||||||
frame_history.on_new_frame(ctx.input(|i| i.time), frame.info().cpu_usage);
|
frame_history.on_new_frame(ctx.input(|i| i.time), frame.info().cpu_usage);
|
||||||
|
@ -598,6 +600,10 @@ impl eframe::App for App {
|
||||||
state.queue_reload = true;
|
state.queue_reload = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
ui.separator();
|
||||||
|
if jobs_menu_ui(ui, jobs, appearance) {
|
||||||
|
*show_jobs = !*show_jobs;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -618,7 +624,6 @@ impl eframe::App for App {
|
||||||
egui::SidePanel::left("side_panel").show(ctx, |ui| {
|
egui::SidePanel::left("side_panel").show(ctx, |ui| {
|
||||||
egui::ScrollArea::both().show(ui, |ui| {
|
egui::ScrollArea::both().show(ui, |ui| {
|
||||||
config_ui(ui, state, show_project_config, config_state, appearance);
|
config_ui(ui, state, show_project_config, config_state, appearance);
|
||||||
jobs_ui(ui, jobs, appearance);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -634,6 +639,7 @@ impl eframe::App for App {
|
||||||
arch_config_window(ctx, state, show_arch_config, appearance);
|
arch_config_window(ctx, state, show_arch_config, appearance);
|
||||||
debug_window(ctx, show_debug, frame_history, appearance);
|
debug_window(ctx, show_debug, frame_history, appearance);
|
||||||
graphics_window(ctx, show_graphics, frame_history, graphics_state, appearance);
|
graphics_window(ctx, show_graphics, frame_history, graphics_state, appearance);
|
||||||
|
jobs_window(ctx, show_jobs, jobs, appearance);
|
||||||
|
|
||||||
self.post_update(ctx);
|
self.post_update(ctx);
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,12 +85,15 @@ impl JobQueue {
|
||||||
/// Clears all finished jobs.
|
/// Clears all finished jobs.
|
||||||
pub fn clear_finished(&mut self) {
|
pub fn clear_finished(&mut self) {
|
||||||
self.jobs.retain(|job| {
|
self.jobs.retain(|job| {
|
||||||
!(job.should_remove
|
!(job.handle.is_none() && job.context.status.read().unwrap().error.is_none())
|
||||||
&& job.handle.is_none()
|
|
||||||
&& job.context.status.read().unwrap().error.is_none())
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Clears all errored jobs.
|
||||||
|
pub fn clear_errored(&mut self) {
|
||||||
|
self.jobs.retain(|job| job.context.status.read().unwrap().error.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
/// Removes a job from the queue given its ID.
|
/// Removes a job from the queue given its ID.
|
||||||
pub fn remove(&mut self, id: usize) { self.jobs.retain(|job| job.id != id); }
|
pub fn remove(&mut self, id: usize) { self.jobs.retain(|job| job.id != id); }
|
||||||
}
|
}
|
||||||
|
@ -107,7 +110,6 @@ pub struct JobState {
|
||||||
pub handle: Option<JoinHandle<JobResult>>,
|
pub handle: Option<JoinHandle<JobResult>>,
|
||||||
pub context: JobContext,
|
pub context: JobContext,
|
||||||
pub cancel: Sender<()>,
|
pub cancel: Sender<()>,
|
||||||
pub should_remove: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
@ -163,7 +165,7 @@ fn start_job(
|
||||||
});
|
});
|
||||||
let id = JOB_ID.fetch_add(1, Ordering::Relaxed);
|
let id = JOB_ID.fetch_add(1, Ordering::Relaxed);
|
||||||
log::info!("Started job {}", id);
|
log::info!("Started job {}", id);
|
||||||
JobState { id, kind, handle: Some(handle), context, cancel: tx, should_remove: true }
|
JobState { id, kind, handle: Some(handle), context, cancel: tx }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_status(
|
fn update_status(
|
||||||
|
|
|
@ -4,7 +4,7 @@ use std::{
|
||||||
sync::mpsc::Receiver,
|
sync::mpsc::Receiver,
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::{anyhow, Context, Error, Result};
|
use anyhow::{anyhow, Error, Result};
|
||||||
use objdiff_core::{
|
use objdiff_core::{
|
||||||
diff::{diff_objs, DiffObjConfig, ObjDiff},
|
diff::{diff_objs, DiffObjConfig, ObjDiff},
|
||||||
obj::{read, ObjInfo},
|
obj::{read, ObjInfo},
|
||||||
|
@ -189,36 +189,46 @@ fn run_build(
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut total = 3;
|
let mut total = 1;
|
||||||
if config.build_target && target_path_rel.is_some() {
|
if config.build_target && target_path_rel.is_some() {
|
||||||
total += 1;
|
total += 1;
|
||||||
}
|
}
|
||||||
if config.build_base && base_path_rel.is_some() {
|
if config.build_base && base_path_rel.is_some() {
|
||||||
total += 1;
|
total += 1;
|
||||||
}
|
}
|
||||||
let first_status = match target_path_rel {
|
if target_path_rel.is_some() {
|
||||||
|
total += 1;
|
||||||
|
}
|
||||||
|
if base_path_rel.is_some() {
|
||||||
|
total += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut step_idx = 0;
|
||||||
|
let mut first_status = match target_path_rel {
|
||||||
Some(target_path_rel) if config.build_target => {
|
Some(target_path_rel) if config.build_target => {
|
||||||
update_status(
|
update_status(
|
||||||
context,
|
context,
|
||||||
format!("Building target {}", target_path_rel.display()),
|
format!("Building target {}", target_path_rel.display()),
|
||||||
0,
|
step_idx,
|
||||||
total,
|
total,
|
||||||
&cancel,
|
&cancel,
|
||||||
)?;
|
)?;
|
||||||
|
step_idx += 1;
|
||||||
run_make(&config.build_config, target_path_rel)
|
run_make(&config.build_config, target_path_rel)
|
||||||
}
|
}
|
||||||
_ => BuildStatus::default(),
|
_ => BuildStatus::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let second_status = match base_path_rel {
|
let mut second_status = match base_path_rel {
|
||||||
Some(base_path_rel) if config.build_base => {
|
Some(base_path_rel) if config.build_base => {
|
||||||
update_status(
|
update_status(
|
||||||
context,
|
context,
|
||||||
format!("Building base {}", base_path_rel.display()),
|
format!("Building base {}", base_path_rel.display()),
|
||||||
0,
|
step_idx,
|
||||||
total,
|
total,
|
||||||
&cancel,
|
&cancel,
|
||||||
)?;
|
)?;
|
||||||
|
step_idx += 1;
|
||||||
run_make(&config.build_config, base_path_rel)
|
run_make(&config.build_config, base_path_rel)
|
||||||
}
|
}
|
||||||
_ => BuildStatus::default(),
|
_ => BuildStatus::default(),
|
||||||
|
@ -226,19 +236,32 @@ fn run_build(
|
||||||
|
|
||||||
let time = OffsetDateTime::now_utc();
|
let time = OffsetDateTime::now_utc();
|
||||||
|
|
||||||
let first_obj =
|
let first_obj = match &obj_config.target_path {
|
||||||
match &obj_config.target_path {
|
|
||||||
Some(target_path) if first_status.success => {
|
Some(target_path) if first_status.success => {
|
||||||
update_status(
|
update_status(
|
||||||
context,
|
context,
|
||||||
format!("Loading target {}", target_path_rel.unwrap().display()),
|
format!("Loading target {}", target_path_rel.unwrap().display()),
|
||||||
2,
|
step_idx,
|
||||||
total,
|
total,
|
||||||
&cancel,
|
&cancel,
|
||||||
)?;
|
)?;
|
||||||
Some(read::read(target_path, &config.diff_obj_config).with_context(|| {
|
step_idx += 1;
|
||||||
format!("Failed to read object '{}'", target_path.display())
|
match read::read(target_path, &config.diff_obj_config) {
|
||||||
})?)
|
Ok(obj) => Some(obj),
|
||||||
|
Err(e) => {
|
||||||
|
first_status = BuildStatus {
|
||||||
|
success: false,
|
||||||
|
stdout: format!("Loading object '{}'", target_path.display()),
|
||||||
|
stderr: format!("{:#}", e),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(_) => {
|
||||||
|
step_idx += 1;
|
||||||
|
None
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
|
@ -248,22 +271,36 @@ fn run_build(
|
||||||
update_status(
|
update_status(
|
||||||
context,
|
context,
|
||||||
format!("Loading base {}", base_path_rel.unwrap().display()),
|
format!("Loading base {}", base_path_rel.unwrap().display()),
|
||||||
3,
|
step_idx,
|
||||||
total,
|
total,
|
||||||
&cancel,
|
&cancel,
|
||||||
)?;
|
)?;
|
||||||
Some(
|
step_idx += 1;
|
||||||
read::read(base_path, &config.diff_obj_config)
|
match read::read(base_path, &config.diff_obj_config) {
|
||||||
.with_context(|| format!("Failed to read object '{}'", base_path.display()))?,
|
Ok(obj) => Some(obj),
|
||||||
)
|
Err(e) => {
|
||||||
|
second_status = BuildStatus {
|
||||||
|
success: false,
|
||||||
|
stdout: format!("Loading object '{}'", base_path.display()),
|
||||||
|
stderr: format!("{:#}", e),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(_) => {
|
||||||
|
step_idx += 1;
|
||||||
|
None
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
update_status(context, "Performing diff".to_string(), 4, total, &cancel)?;
|
update_status(context, "Performing diff".to_string(), step_idx, total, &cancel)?;
|
||||||
|
step_idx += 1;
|
||||||
let result = diff_objs(&config.diff_obj_config, first_obj.as_ref(), second_obj.as_ref(), None)?;
|
let result = diff_objs(&config.diff_obj_config, first_obj.as_ref(), second_obj.as_ref(), None)?;
|
||||||
|
|
||||||
update_status(context, "Complete".to_string(), total, total, &cancel)?;
|
update_status(context, "Complete".to_string(), step_idx, total, &cancel)?;
|
||||||
Ok(Box::new(ObjDiffResult {
|
Ok(Box::new(ObjDiffResult {
|
||||||
first_status,
|
first_status,
|
||||||
second_status,
|
second_status,
|
||||||
|
@ -274,7 +311,7 @@ fn run_build(
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn start_build(ctx: &egui::Context, config: ObjDiffConfig) -> JobState {
|
pub fn start_build(ctx: &egui::Context, config: ObjDiffConfig) -> JobState {
|
||||||
start_job(ctx, "Object diff", Job::ObjDiff, move |context, cancel| {
|
start_job(ctx, "Build", Job::ObjDiff, move |context, cancel| {
|
||||||
run_build(&context, cancel, config).map(|result| JobResult::ObjDiff(Some(result)))
|
run_build(&context, cancel, config).map(|result| JobResult::ObjDiff(Some(result)))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,29 @@
|
||||||
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
use egui::{ProgressBar, RichText, Widget};
|
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) {
|
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 remove_job: Option<usize> = None;
|
||||||
|
let mut any_jobs = false;
|
||||||
for job in jobs.iter_mut() {
|
for job in jobs.iter_mut() {
|
||||||
let Ok(status) = job.context.status.read() else {
|
let Ok(status) = job.context.status.read() else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
ui.group(|ui| {
|
any_jobs = true;
|
||||||
|
ui.separator();
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label(&status.title);
|
ui.label(&status.title);
|
||||||
if ui.small_button("✖").clicked() {
|
if ui.small_button("✖").clicked() {
|
||||||
if job.handle.is_some() {
|
if job.handle.is_some() {
|
||||||
job.should_remove = true;
|
|
||||||
if let Err(e) = job.cancel.send(()) {
|
if let Err(e) = job.cancel.send(()) {
|
||||||
log::error!("Failed to cancel job: {e:?}");
|
log::error!("Failed to cancel job: {e:?}");
|
||||||
}
|
}
|
||||||
|
@ -40,19 +48,115 @@ pub fn jobs_ui(ui: &mut egui::Ui, jobs: &mut JobQueue, appearance: &Appearance)
|
||||||
format!("Error: {:width$}", err_string, width = STATUS_LENGTH - 7)
|
format!("Error: {:width$}", err_string, width = STATUS_LENGTH - 7)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.on_hover_text_at_pointer(RichText::new(err_string).color(appearance.delete_color));
|
.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 {
|
} else {
|
||||||
ui.label(if status.status.len() > STATUS_LENGTH - 3 {
|
ui.label(if status.status.len() > STATUS_LENGTH - 3 {
|
||||||
format!("{}…", &status.status[0..STATUS_LENGTH - 3])
|
format!("{}…", &status.status[0..STATUS_LENGTH - 3])
|
||||||
} else {
|
} else {
|
||||||
format!("{:width$}", &status.status, width = STATUS_LENGTH)
|
format!("{:width$}", &status.status, width = STATUS_LENGTH)
|
||||||
})
|
})
|
||||||
.on_hover_text_at_pointer(&status.status);
|
.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 {
|
if let Some(idx) = remove_job {
|
||||||
jobs.remove(idx);
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -452,7 +452,7 @@ fn symbol_list_ui(
|
||||||
fn build_log_ui(ui: &mut Ui, status: &BuildStatus, appearance: &Appearance) {
|
fn build_log_ui(ui: &mut Ui, status: &BuildStatus, appearance: &Appearance) {
|
||||||
ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
|
ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
|
||||||
ui.horizontal(|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));
|
ui.output_mut(|output| output.copied_text.clone_from(&status.cmdline));
|
||||||
}
|
}
|
||||||
if ui.button("Copy log").clicked() {
|
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().override_text_style = Some(egui::TextStyle::Monospace);
|
||||||
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
|
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
|
||||||
|
|
||||||
|
if !status.cmdline.is_empty() {
|
||||||
ui.label(&status.cmdline);
|
ui.label(&status.cmdline);
|
||||||
|
}
|
||||||
|
if !status.stdout.is_empty() {
|
||||||
ui.colored_label(appearance.replace_color, &status.stdout);
|
ui.colored_label(appearance.replace_color, &status.stdout);
|
||||||
|
}
|
||||||
|
if !status.stderr.is_empty() {
|
||||||
ui.colored_label(appearance.delete_color, &status.stderr);
|
ui.colored_label(appearance.delete_color, &status.stderr);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue