Version 0.2.0

- Update checker & auto-updater
- Configure font sizes and diff colors
- Data diffing bug fixes & improvements
- Bug fix for low match percent
- Improvements to Jobs UI (cancel, dismiss errors)
- "Demangle" tool

Closes #6, #13, #17, #19
This commit is contained in:
2022-12-06 17:53:32 -05:00
parent 2f2efb4711
commit 771a141110
26 changed files with 2244 additions and 485 deletions

View File

@@ -2,22 +2,23 @@ use std::{
default::Default,
ffi::OsStr,
path::{Path, PathBuf},
rc::Rc,
sync::{
atomic::{AtomicBool, Ordering},
Arc, RwLock,
Arc, Mutex, RwLock,
},
time::Duration,
};
use eframe::Frame;
use egui::Widget;
use egui::{Color32, FontFamily, FontId, TextStyle};
use notify::{RecursiveMode, Watcher};
use time::{OffsetDateTime, UtcOffset};
use crate::{
jobs::{
check_update::{queue_check_update, CheckUpdateResult},
objdiff::{queue_build, BuildStatus, ObjDiffResult},
Job, JobResult, JobState,
Job, JobResult, JobState, JobStatus,
},
views::{
config::config_ui, data_diff::data_diff_ui, function_diff::function_diff_ui, jobs::jobs_ui,
@@ -48,6 +49,36 @@ pub struct DiffConfig {
// pub mapped_symbols: HashMap<String, String>,
}
const DEFAULT_COLOR_ROTATION: [Color32; 9] = [
Color32::from_rgb(255, 0, 255),
Color32::from_rgb(0, 255, 255),
Color32::from_rgb(0, 128, 0),
Color32::from_rgb(255, 0, 0),
Color32::from_rgb(255, 255, 0),
Color32::from_rgb(255, 192, 203),
Color32::from_rgb(0, 0, 255),
Color32::from_rgb(0, 255, 0),
Color32::from_rgb(128, 128, 128),
];
#[derive(serde::Deserialize, serde::Serialize)]
#[serde(default)]
pub struct ViewConfig {
pub ui_font: FontId,
pub code_font: FontId,
pub diff_colors: Vec<Color32>,
}
impl Default for ViewConfig {
fn default() -> Self {
Self {
ui_font: FontId { size: 14.0, family: FontFamily::Proportional },
code_font: FontId { size: 14.0, family: FontFamily::Monospace },
diff_colors: DEFAULT_COLOR_ROTATION.to_vec(),
}
}
}
#[derive(serde::Deserialize, serde::Serialize)]
#[serde(default)]
pub struct ViewState {
@@ -64,14 +95,21 @@ pub struct ViewState {
#[serde(skip)]
pub show_config: bool,
#[serde(skip)]
pub show_demangle: bool,
#[serde(skip)]
pub demangle_text: String,
#[serde(skip)]
pub diff_config: DiffConfig,
#[serde(skip)]
pub search: String,
#[serde(skip)]
pub utc_offset: UtcOffset,
#[serde(skip)]
pub check_update: Option<Box<CheckUpdateResult>>,
// Config
pub diff_kind: DiffKind,
pub reverse_fn_order: bool,
pub view_config: ViewConfig,
}
impl Default for ViewState {
@@ -83,11 +121,15 @@ impl Default for ViewState {
selected_symbol: None,
current_view: Default::default(),
show_config: false,
show_demangle: false,
demangle_text: String::new(),
diff_config: Default::default(),
search: Default::default(),
utc_offset: UtcOffset::UTC,
check_update: None,
diff_kind: Default::default(),
reverse_fn_order: false,
view_config: Default::default(),
}
}
}
@@ -95,7 +137,7 @@ impl Default for ViewState {
#[derive(Default, Clone, serde::Deserialize, serde::Serialize)]
#[serde(default)]
pub struct AppConfig {
pub custom_make: String,
pub custom_make: Option<String>,
// WSL2 settings
#[serde(skip)]
pub available_wsl_distros: Option<Vec<String>>,
@@ -111,6 +153,19 @@ pub struct AppConfig {
pub right_obj: Option<PathBuf>,
#[serde(skip)]
pub project_dir_change: bool,
#[serde(skip)]
pub queue_update_check: bool,
pub auto_update_check: bool,
}
#[derive(Default, Clone, serde::Deserialize)]
#[serde(default)]
pub struct ProjectConfig {
pub custom_make: Option<String>,
pub project_dir: Option<PathBuf>,
pub target_obj_dir: Option<PathBuf>,
pub base_obj_dir: Option<PathBuf>,
pub build_target: bool,
}
/// We derive Deserialize/Serialize so we can persist app state on shutdown.
@@ -124,6 +179,10 @@ pub struct App {
modified: Arc<AtomicBool>,
#[serde(skip)]
watcher: Option<notify::RecommendedWatcher>,
#[serde(skip)]
relaunch_path: Rc<Mutex<Option<PathBuf>>>,
#[serde(skip)]
should_relaunch: bool,
}
impl Default for App {
@@ -133,6 +192,8 @@ impl Default for App {
config: Arc::new(Default::default()),
modified: Arc::new(Default::default()),
watcher: None,
relaunch_path: Default::default(),
should_relaunch: false,
}
}
}
@@ -141,7 +202,11 @@ const CONFIG_KEY: &str = "app_config";
impl App {
/// Called once before the first frame.
pub fn new(cc: &eframe::CreationContext<'_>, utc_offset: UtcOffset) -> Self {
pub fn new(
cc: &eframe::CreationContext<'_>,
utc_offset: UtcOffset,
relaunch_path: Rc<Mutex<Option<PathBuf>>>,
) -> Self {
// This is also where you can customized the look at feel of egui using
// `cc.egui_ctx.set_visuals` and `cc.egui_ctx.set_fonts`.
@@ -153,11 +218,16 @@ impl App {
if config.project_dir.is_some() {
config.project_dir_change = true;
}
config.queue_update_check = config.auto_update_check;
app.config = Arc::new(RwLock::new(config));
app.view_state.utc_offset = utc_offset;
app.relaunch_path = relaunch_path;
app
} else {
Self::default()
let mut app = Self::default();
app.view_state.utc_offset = utc_offset;
app.relaunch_path = relaunch_path;
app
}
}
}
@@ -165,17 +235,44 @@ impl App {
impl eframe::App for App {
/// Called each time the UI needs repainting, which may be many times per second.
/// Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`.
fn update(&mut self, ctx: &egui::Context, frame: &mut Frame) {
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
if self.should_relaunch {
frame.close();
return;
}
let Self { config, view_state, .. } = self;
{
let config = &view_state.view_config;
let mut style = (*ctx.style()).clone();
style.text_styles.insert(TextStyle::Body, FontId {
size: (config.ui_font.size * 0.75).floor(),
family: config.ui_font.family.clone(),
});
style.text_styles.insert(TextStyle::Body, config.ui_font.clone());
style.text_styles.insert(TextStyle::Button, config.ui_font.clone());
style.text_styles.insert(TextStyle::Heading, FontId {
size: (config.ui_font.size * 1.5).floor(),
family: config.ui_font.family.clone(),
});
style.text_styles.insert(TextStyle::Monospace, config.code_font.clone());
ctx.set_style(style);
}
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
egui::menu::bar(ui, |ui| {
ui.menu_button("File", |ui| {
if ui.button("Show config").clicked() {
view_state.show_config = !view_state.show_config;
}
if ui.button("Quit").clicked() {
frame.close();
}
if ui.button("Show config").clicked() {
view_state.show_config = !view_state.show_config;
});
ui.menu_button("Tools", |ui| {
if ui.button("Demangle").clicked() {
view_state.show_demangle = !view_state.show_demangle;
}
});
});
@@ -221,31 +318,55 @@ impl eframe::App for App {
}
egui::Window::new("Config").open(&mut view_state.show_config).show(ctx, |ui| {
ui.label("Diff type:");
if egui::RadioButton::new(
view_state.diff_kind == DiffKind::SplitObj,
"Split object diff",
)
.ui(ui)
.on_hover_text("Compare individual object files")
.clicked()
{
view_state.diff_kind = DiffKind::SplitObj;
}
if egui::RadioButton::new(
view_state.diff_kind == DiffKind::WholeBinary,
"Whole binary diff",
)
.ui(ui)
.on_hover_text("Compare two full binaries")
.clicked()
{
view_state.diff_kind = DiffKind::WholeBinary;
}
ui.label("UI font:");
egui::introspection::font_id_ui(ui, &mut view_state.view_config.ui_font);
ui.separator();
ui.label("Code font:");
egui::introspection::font_id_ui(ui, &mut view_state.view_config.code_font);
ui.separator();
ui.label("Diff colors:");
if ui.button("Reset").clicked() {
view_state.view_config.diff_colors = DEFAULT_COLOR_ROTATION.to_vec();
}
let mut remove_at: Option<usize> = None;
let num_colors = view_state.view_config.diff_colors.len();
for (idx, color) in view_state.view_config.diff_colors.iter_mut().enumerate() {
ui.horizontal(|ui| {
ui.color_edit_button_srgba(color);
if num_colors > 1 {
if ui.small_button("-").clicked() {
remove_at = Some(idx);
}
}
});
}
if let Some(idx) = remove_at {
view_state.view_config.diff_colors.remove(idx);
}
if ui.small_button("+").clicked() {
view_state.view_config.diff_colors.push(Color32::BLACK);
}
});
egui::Window::new("Demangle").open(&mut view_state.show_demangle).show(ctx, |ui| {
ui.text_edit_singleline(&mut view_state.demangle_text);
ui.add_space(10.0);
if let Some(demangled) =
cwdemangle::demangle(&view_state.demangle_text, &Default::default())
{
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(TextStyle::Monospace);
ui.colored_label(Color32::LIGHT_BLUE, &demangled);
});
if ui.button("Copy").clicked() {
ui.output().copied_text = demangled;
}
} else {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(TextStyle::Monospace);
ui.colored_label(Color32::LIGHT_RED, "[invalid]");
});
}
});
// Windows + request_repaint_after breaks dialogs:
@@ -272,7 +393,7 @@ impl eframe::App for App {
eframe::set_value(storage, eframe::APP_KEY, self);
}
fn post_rendering(&mut self, _window_size_px: [u32; 2], _frame: &Frame) {
fn post_rendering(&mut self, _window_size_px: [u32; 2], _frame: &eframe::Frame) {
for job in &mut self.view_state.jobs {
if let Some(handle) = &job.handle {
if !handle.is_finished() {
@@ -305,10 +426,38 @@ impl eframe::App for App {
time: OffsetDateTime::now_utc(),
}));
}
JobResult::CheckUpdate(state) => {
self.view_state.check_update = Some(state);
}
JobResult::Update(state) => {
if let Ok(mut guard) = self.relaunch_path.lock() {
*guard = Some(state.exe_path);
}
self.should_relaunch = true;
}
}
}
Err(e) => {
log::error!("Failed to join job handle: {:?}", e);
Err(err) => {
let err = if let Some(msg) = err.downcast_ref::<&'static str>() {
anyhow::Error::msg(*msg)
} else if let Some(msg) = err.downcast_ref::<String>() {
anyhow::Error::msg(msg.clone())
} else {
anyhow::Error::msg("Thread panicked")
};
let result = job.status.write();
if let Ok(mut guard) = result {
guard.error = Some(err);
} else {
drop(result);
job.status = Arc::new(RwLock::new(JobStatus {
title: "Error".to_string(),
progress_percent: 0.0,
progress_items: None,
status: "".to_string(),
error: Some(err),
}));
}
}
}
}
@@ -355,6 +504,11 @@ impl eframe::App for App {
}
self.modified.store(false, Ordering::Relaxed);
}
if config.queue_update_check {
self.view_state.jobs.push(queue_check_update());
config.queue_update_check = false;
}
}
}
}