mirror of
https://github.com/encounter/objdiff.git
synced 2025-12-17 08:57:25 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 08cd768260 | |||
| 8acaaf528c | |||
| 6e881a74e1 | |||
| cc1bc44e69 | |||
| c7b85518ab | |||
| bb039a1445 | |||
| 8fc142d316 | |||
| b0123b3f83 | |||
| 2ec17aee9b | |||
| ec9731e1e5 |
5
.github/workflows/build.yaml
vendored
5
.github/workflows/build.yaml
vendored
@@ -110,11 +110,6 @@ jobs:
|
|||||||
name: linux-aarch64
|
name: linux-aarch64
|
||||||
build: zigbuild
|
build: zigbuild
|
||||||
features: default
|
features: default
|
||||||
- platform: ubuntu-latest
|
|
||||||
target: armv7-unknown-linux-musleabi
|
|
||||||
name: linux-armv7l
|
|
||||||
build: zigbuild
|
|
||||||
features: default
|
|
||||||
- platform: windows-latest
|
- platform: windows-latest
|
||||||
target: i686-pc-windows-msvc
|
target: i686-pc-windows-msvc
|
||||||
name: windows-x86
|
name: windows-x86
|
||||||
|
|||||||
63
Cargo.lock
generated
63
Cargo.lock
generated
@@ -2084,6 +2084,25 @@ version = "2.10.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "187674a687eed5fe42285b40c6291f9a01517d415fad1c3cbc6a9f778af7fcd4"
|
checksum = "187674a687eed5fe42285b40c6291f9a01517d415fad1c3cbc6a9f778af7fcd4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is-docker"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is-wsl"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
|
||||||
|
dependencies = [
|
||||||
|
"is-docker",
|
||||||
|
"once_cell",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "is_ci"
|
name = "is_ci"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@@ -2204,6 +2223,16 @@ dependencies = [
|
|||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libmimalloc-sys"
|
||||||
|
version = "0.1.39"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "23aa6811d3bd4deb8a84dde645f943476d13b248d818edcf8ce0b2f37f036b44"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libredox"
|
name = "libredox"
|
||||||
version = "0.0.2"
|
version = "0.0.2"
|
||||||
@@ -2320,6 +2349,15 @@ dependencies = [
|
|||||||
"paste",
|
"paste",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mimalloc"
|
||||||
|
version = "0.1.43"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "68914350ae34959d83f732418d51e2427a794055d0b9529f48259ac07af65633"
|
||||||
|
dependencies = [
|
||||||
|
"libmimalloc-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mime"
|
name = "mime"
|
||||||
version = "0.3.17"
|
version = "0.3.17"
|
||||||
@@ -2823,13 +2861,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objdiff-cli"
|
name = "objdiff-cli"
|
||||||
version = "2.1.0"
|
version = "2.2.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"argp",
|
"argp",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
"enable-ansi-support",
|
"enable-ansi-support",
|
||||||
"memmap2",
|
"memmap2",
|
||||||
|
"mimalloc",
|
||||||
"objdiff-core",
|
"objdiff-core",
|
||||||
"prost",
|
"prost",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
@@ -2844,7 +2883,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objdiff-core"
|
name = "objdiff-core"
|
||||||
version = "2.1.0"
|
version = "2.2.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"arm-attr",
|
"arm-attr",
|
||||||
@@ -2883,7 +2922,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objdiff-gui"
|
name = "objdiff-gui"
|
||||||
version = "2.1.0"
|
version = "2.2.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -2904,6 +2943,7 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"notify",
|
"notify",
|
||||||
"objdiff-core",
|
"objdiff-core",
|
||||||
|
"open",
|
||||||
"path-slash",
|
"path-slash",
|
||||||
"png",
|
"png",
|
||||||
"pollster",
|
"pollster",
|
||||||
@@ -2941,6 +2981,17 @@ version = "1.19.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "open"
|
||||||
|
version = "5.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "61a877bf6abd716642a53ef1b89fb498923a4afca5c754f9050b4d081c05c4b3"
|
||||||
|
dependencies = [
|
||||||
|
"is-wsl",
|
||||||
|
"libc",
|
||||||
|
"pathdiff",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl"
|
name = "openssl"
|
||||||
version = "0.10.66"
|
version = "0.10.66"
|
||||||
@@ -3066,6 +3117,12 @@ version = "0.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1e91099d4268b0e11973f036e885d652fb0b21fedcf69738c627f94db6a44f42"
|
checksum = "1e91099d4268b0e11973f036e885d652fb0b21fedcf69738c627f94db6a44f42"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pathdiff"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pathfinder_geometry"
|
name = "pathfinder_geometry"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ strip = "debuginfo"
|
|||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "2.1.0"
|
version = "2.2.1"
|
||||||
authors = ["Luke Street <luke@street.dev>"]
|
authors = ["Luke Street <luke@street.dev>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
|
|||||||
@@ -28,3 +28,6 @@ supports-color = "3.0"
|
|||||||
time = { version = "0.3", features = ["formatting", "local-offset"] }
|
time = { version = "0.3", features = ["formatting", "local-offset"] }
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
|
||||||
|
[target.'cfg(target_env = "musl")'.dependencies]
|
||||||
|
mimalloc = "0.1"
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ fn report_object(
|
|||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
auto_generated: object.metadata.as_ref().and_then(|m| m.auto_generated),
|
auto_generated: object.metadata.as_ref().and_then(|m| m.auto_generated),
|
||||||
};
|
};
|
||||||
let mut measures = Measures::default();
|
let mut measures = Measures { total_units: 1, ..Default::default() };
|
||||||
let mut sections = vec![];
|
let mut sections = vec![];
|
||||||
let mut functions = vec![];
|
let mut functions = vec![];
|
||||||
|
|
||||||
@@ -280,6 +280,7 @@ fn report_object(
|
|||||||
if metadata.complete.unwrap_or(false) {
|
if metadata.complete.unwrap_or(false) {
|
||||||
measures.complete_code = measures.total_code;
|
measures.complete_code = measures.total_code;
|
||||||
measures.complete_data = measures.total_data;
|
measures.complete_data = measures.total_data;
|
||||||
|
measures.complete_units = 1;
|
||||||
}
|
}
|
||||||
measures.calc_fuzzy_match_percent();
|
measures.calc_fuzzy_match_percent();
|
||||||
measures.calc_matched_percent();
|
measures.calc_matched_percent();
|
||||||
|
|||||||
@@ -2,6 +2,12 @@ mod argp_version;
|
|||||||
mod cmd;
|
mod cmd;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
|
// musl's allocator is very slow, so use mimalloc when targeting musl.
|
||||||
|
// Otherwise, use the system allocator to avoid extra code size.
|
||||||
|
#[cfg(target_env = "musl")]
|
||||||
|
#[global_allocator]
|
||||||
|
static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
||||||
|
|
||||||
use std::{env, ffi::OsStr, fmt::Display, path::PathBuf, str::FromStr};
|
use std::{env, ffi::OsStr, fmt::Display, path::PathBuf, str::FromStr};
|
||||||
|
|
||||||
use anyhow::{Error, Result};
|
use anyhow::{Error, Result};
|
||||||
|
|||||||
Binary file not shown.
@@ -32,6 +32,10 @@ message Measures {
|
|||||||
uint64 complete_data = 13;
|
uint64 complete_data = 13;
|
||||||
// Completed (or "linked") data percent
|
// Completed (or "linked") data percent
|
||||||
float complete_data_percent = 14;
|
float complete_data_percent = 14;
|
||||||
|
// Total number of units
|
||||||
|
uint32 total_units = 15;
|
||||||
|
// Completed (or "linked") units
|
||||||
|
uint32 complete_units = 16;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Project progress report
|
// Project progress report
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ use serde_json::error::Category;
|
|||||||
include!(concat!(env!("OUT_DIR"), "/objdiff.report.rs"));
|
include!(concat!(env!("OUT_DIR"), "/objdiff.report.rs"));
|
||||||
include!(concat!(env!("OUT_DIR"), "/objdiff.report.serde.rs"));
|
include!(concat!(env!("OUT_DIR"), "/objdiff.report.serde.rs"));
|
||||||
|
|
||||||
pub const REPORT_VERSION: u32 = 1;
|
pub const REPORT_VERSION: u32 = 2;
|
||||||
|
|
||||||
impl Report {
|
impl Report {
|
||||||
|
/// Attempts to parse the report as binary protobuf or JSON.
|
||||||
pub fn parse(data: &[u8]) -> Result<Self> {
|
pub fn parse(data: &[u8]) -> Result<Self> {
|
||||||
if data.is_empty() {
|
if data.is_empty() {
|
||||||
bail!(std::io::Error::from(std::io::ErrorKind::UnexpectedEof));
|
bail!(std::io::Error::from(std::io::ErrorKind::UnexpectedEof));
|
||||||
@@ -25,6 +26,7 @@ impl Report {
|
|||||||
Ok(report)
|
Ok(report)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Attempts to parse the report as JSON, migrating from the legacy report format if necessary.
|
||||||
fn from_json(bytes: &[u8]) -> Result<Self, serde_json::Error> {
|
fn from_json(bytes: &[u8]) -> Result<Self, serde_json::Error> {
|
||||||
match serde_json::from_slice::<Self>(bytes) {
|
match serde_json::from_slice::<Self>(bytes) {
|
||||||
Ok(report) => Ok(report),
|
Ok(report) => Ok(report),
|
||||||
@@ -43,16 +45,23 @@ impl Report {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Migrates the report to the latest version.
|
||||||
|
/// Fails if the report version is newer than supported.
|
||||||
pub fn migrate(&mut self) -> Result<()> {
|
pub fn migrate(&mut self) -> Result<()> {
|
||||||
if self.version == 0 {
|
if self.version == 0 {
|
||||||
self.migrate_v0()?;
|
self.migrate_v0()?;
|
||||||
}
|
}
|
||||||
|
if self.version == 1 {
|
||||||
|
self.migrate_v1()?;
|
||||||
|
}
|
||||||
if self.version != REPORT_VERSION {
|
if self.version != REPORT_VERSION {
|
||||||
bail!("Unsupported report version: {}", self.version);
|
bail!("Unsupported report version: {}", self.version);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Adds `complete_code`, `complete_data`, `complete_code_percent`, and `complete_data_percent`
|
||||||
|
/// to measures, and sets `progress_categories` in unit metadata.
|
||||||
fn migrate_v0(&mut self) -> Result<()> {
|
fn migrate_v0(&mut self) -> Result<()> {
|
||||||
let Some(measures) = &mut self.measures else {
|
let Some(measures) = &mut self.measures else {
|
||||||
bail!("Missing measures in report");
|
bail!("Missing measures in report");
|
||||||
@@ -61,15 +70,16 @@ impl Report {
|
|||||||
let Some(unit_measures) = &mut unit.measures else {
|
let Some(unit_measures) = &mut unit.measures else {
|
||||||
bail!("Missing measures in report unit");
|
bail!("Missing measures in report unit");
|
||||||
};
|
};
|
||||||
let Some(metadata) = &mut unit.metadata else {
|
let mut complete = false;
|
||||||
bail!("Missing metadata in report unit");
|
if let Some(metadata) = &mut unit.metadata {
|
||||||
|
if metadata.module_name.is_some() || metadata.module_id.is_some() {
|
||||||
|
metadata.progress_categories = vec!["modules".to_string()];
|
||||||
|
} else {
|
||||||
|
metadata.progress_categories = vec!["dol".to_string()];
|
||||||
|
}
|
||||||
|
complete = metadata.complete.unwrap_or(false);
|
||||||
};
|
};
|
||||||
if metadata.module_name.is_some() || metadata.module_id.is_some() {
|
if complete {
|
||||||
metadata.progress_categories = vec!["modules".to_string()];
|
|
||||||
} else {
|
|
||||||
metadata.progress_categories = vec!["dol".to_string()];
|
|
||||||
}
|
|
||||||
if metadata.complete.unwrap_or(false) {
|
|
||||||
unit_measures.complete_code = unit_measures.total_code;
|
unit_measures.complete_code = unit_measures.total_code;
|
||||||
unit_measures.complete_data = unit_measures.total_data;
|
unit_measures.complete_data = unit_measures.total_data;
|
||||||
unit_measures.complete_code_percent = 100.0;
|
unit_measures.complete_code_percent = 100.0;
|
||||||
@@ -84,10 +94,42 @@ impl Report {
|
|||||||
measures.complete_data += unit_measures.complete_data;
|
measures.complete_data += unit_measures.complete_data;
|
||||||
}
|
}
|
||||||
measures.calc_matched_percent();
|
measures.calc_matched_percent();
|
||||||
|
self.calculate_progress_categories();
|
||||||
self.version = 1;
|
self.version = 1;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Adds `total_units` and `complete_units` to measures.
|
||||||
|
fn migrate_v1(&mut self) -> Result<()> {
|
||||||
|
let Some(total_measures) = &mut self.measures else {
|
||||||
|
bail!("Missing measures in report");
|
||||||
|
};
|
||||||
|
for unit in &mut self.units {
|
||||||
|
let Some(measures) = &mut unit.measures else {
|
||||||
|
bail!("Missing measures in report unit");
|
||||||
|
};
|
||||||
|
let complete = unit.metadata.as_ref().and_then(|m| m.complete).unwrap_or(false) as u32;
|
||||||
|
let progress_categories =
|
||||||
|
unit.metadata.as_ref().map(|m| m.progress_categories.as_slice()).unwrap_or(&[]);
|
||||||
|
measures.total_units = 1;
|
||||||
|
measures.complete_units = complete;
|
||||||
|
total_measures.total_units += 1;
|
||||||
|
total_measures.complete_units += complete;
|
||||||
|
for id in progress_categories {
|
||||||
|
if let Some(category) = self.categories.iter_mut().find(|c| &c.id == id) {
|
||||||
|
let Some(measures) = &mut category.measures else {
|
||||||
|
bail!("Missing measures in category");
|
||||||
|
};
|
||||||
|
measures.total_units += 1;
|
||||||
|
measures.complete_units += complete;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.version = 2;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculate progress categories based on unit metadata.
|
||||||
pub fn calculate_progress_categories(&mut self) {
|
pub fn calculate_progress_categories(&mut self) {
|
||||||
for unit in &self.units {
|
for unit in &self.units {
|
||||||
let Some(metadata) = unit.metadata.as_ref() else {
|
let Some(metadata) = unit.metadata.as_ref() else {
|
||||||
@@ -242,6 +284,8 @@ impl AddAssign for Measures {
|
|||||||
self.matched_functions += other.matched_functions;
|
self.matched_functions += other.matched_functions;
|
||||||
self.complete_code += other.complete_code;
|
self.complete_code += other.complete_code;
|
||||||
self.complete_data += other.complete_data;
|
self.complete_data += other.complete_data;
|
||||||
|
self.total_units += other.total_units;
|
||||||
|
self.complete_units += other.complete_units;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::{
|
use std::{
|
||||||
fs::File,
|
fs::File,
|
||||||
io::Read,
|
io::{BufReader, Read},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -124,6 +124,10 @@ impl ProjectObject {
|
|||||||
pub fn hidden(&self) -> bool {
|
pub fn hidden(&self) -> bool {
|
||||||
self.metadata.as_ref().and_then(|m| m.auto_generated).unwrap_or(false)
|
self.metadata.as_ref().and_then(|m| m.auto_generated).unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn source_path(&self) -> Option<&String> {
|
||||||
|
self.metadata.as_ref().and_then(|m| m.source_path.as_ref())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
#[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||||
@@ -156,7 +160,7 @@ pub struct ProjectConfigInfo {
|
|||||||
pub fn try_project_config(dir: &Path) -> Option<(Result<ProjectConfig>, ProjectConfigInfo)> {
|
pub fn try_project_config(dir: &Path) -> Option<(Result<ProjectConfig>, ProjectConfigInfo)> {
|
||||||
for filename in CONFIG_FILENAMES.iter() {
|
for filename in CONFIG_FILENAMES.iter() {
|
||||||
let config_path = dir.join(filename);
|
let config_path = dir.join(filename);
|
||||||
let Ok(mut file) = File::open(&config_path) else {
|
let Ok(file) = File::open(&config_path) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let metadata = file.metadata();
|
let metadata = file.metadata();
|
||||||
@@ -165,9 +169,10 @@ pub fn try_project_config(dir: &Path) -> Option<(Result<ProjectConfig>, ProjectC
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let ts = FileTime::from_last_modification_time(&metadata);
|
let ts = FileTime::from_last_modification_time(&metadata);
|
||||||
|
let mut reader = BufReader::new(file);
|
||||||
let mut result = match filename.contains("json") {
|
let mut result = match filename.contains("json") {
|
||||||
true => read_json_config(&mut file),
|
true => read_json_config(&mut reader),
|
||||||
false => read_yml_config(&mut file),
|
false => read_yml_config(&mut reader),
|
||||||
};
|
};
|
||||||
if let Ok(config) = &result {
|
if let Ok(config) = &result {
|
||||||
// Validate min_version if present
|
// Validate min_version if present
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ globset = { version = "0.4", features = ["serde1"] }
|
|||||||
log = "0.4"
|
log = "0.4"
|
||||||
notify = { git = "https://github.com/notify-rs/notify", rev = "128bf6230c03d39dbb7f301ff7b20e594e34c3a2" }
|
notify = { git = "https://github.com/notify-rs/notify", rev = "128bf6230c03d39dbb7f301ff7b20e594e34c3a2" }
|
||||||
objdiff-core = { path = "../objdiff-core", features = ["all"] }
|
objdiff-core = { path = "../objdiff-core", features = ["all"] }
|
||||||
|
open = "5.3"
|
||||||
png = "0.17"
|
png = "0.17"
|
||||||
pollster = "0.3"
|
pollster = "0.3"
|
||||||
regex = "1.10"
|
regex = "1.10"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use std::{
|
|||||||
atomic::{AtomicBool, Ordering},
|
atomic::{AtomicBool, Ordering},
|
||||||
Arc, Mutex, RwLock,
|
Arc, Mutex, RwLock,
|
||||||
},
|
},
|
||||||
|
time::Instant,
|
||||||
};
|
};
|
||||||
|
|
||||||
use filetime::FileTime;
|
use filetime::FileTime;
|
||||||
@@ -39,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},
|
||||||
},
|
},
|
||||||
@@ -61,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.
|
||||||
@@ -72,6 +74,7 @@ pub struct ObjectConfig {
|
|||||||
pub reverse_fn_order: Option<bool>,
|
pub reverse_fn_order: Option<bool>,
|
||||||
pub complete: Option<bool>,
|
pub complete: Option<bool>,
|
||||||
pub scratch: Option<ScratchConfig>,
|
pub scratch: Option<ScratchConfig>,
|
||||||
|
pub source_path: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -82,6 +85,36 @@ fn default_watch_patterns() -> Vec<Glob> {
|
|||||||
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
|
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct AppState {
|
||||||
|
pub config: AppConfig,
|
||||||
|
pub objects: Vec<ProjectObject>,
|
||||||
|
pub object_nodes: Vec<ProjectObjectNode>,
|
||||||
|
pub watcher_change: bool,
|
||||||
|
pub config_change: bool,
|
||||||
|
pub obj_change: bool,
|
||||||
|
pub queue_build: bool,
|
||||||
|
pub queue_reload: bool,
|
||||||
|
pub project_config_info: Option<ProjectConfigInfo>,
|
||||||
|
pub last_mod_check: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AppState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
config: Default::default(),
|
||||||
|
objects: vec![],
|
||||||
|
object_nodes: vec![],
|
||||||
|
watcher_change: false,
|
||||||
|
config_change: false,
|
||||||
|
obj_change: false,
|
||||||
|
queue_build: false,
|
||||||
|
queue_reload: false,
|
||||||
|
project_config_info: None,
|
||||||
|
last_mod_check: Instant::now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, serde::Deserialize, serde::Serialize)]
|
#[derive(Clone, serde::Deserialize, serde::Serialize)]
|
||||||
pub struct AppConfig {
|
pub struct AppConfig {
|
||||||
// TODO: https://github.com/ron-rs/ron/pull/455
|
// TODO: https://github.com/ron-rs/ron/pull/455
|
||||||
@@ -116,23 +149,6 @@ pub struct AppConfig {
|
|||||||
pub recent_projects: Vec<PathBuf>,
|
pub recent_projects: Vec<PathBuf>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub diff_obj_config: DiffObjConfig,
|
pub diff_obj_config: DiffObjConfig,
|
||||||
|
|
||||||
#[serde(skip)]
|
|
||||||
pub objects: Vec<ProjectObject>,
|
|
||||||
#[serde(skip)]
|
|
||||||
pub object_nodes: Vec<ProjectObjectNode>,
|
|
||||||
#[serde(skip)]
|
|
||||||
pub watcher_change: bool,
|
|
||||||
#[serde(skip)]
|
|
||||||
pub config_change: bool,
|
|
||||||
#[serde(skip)]
|
|
||||||
pub obj_change: bool,
|
|
||||||
#[serde(skip)]
|
|
||||||
pub queue_build: bool,
|
|
||||||
#[serde(skip)]
|
|
||||||
pub queue_reload: bool,
|
|
||||||
#[serde(skip)]
|
|
||||||
pub project_config_info: Option<ProjectConfigInfo>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for AppConfig {
|
impl Default for AppConfig {
|
||||||
@@ -153,30 +169,22 @@ impl Default for AppConfig {
|
|||||||
watch_patterns: DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect(),
|
watch_patterns: DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect(),
|
||||||
recent_projects: vec![],
|
recent_projects: vec![],
|
||||||
diff_obj_config: Default::default(),
|
diff_obj_config: Default::default(),
|
||||||
objects: vec![],
|
|
||||||
object_nodes: vec![],
|
|
||||||
watcher_change: false,
|
|
||||||
config_change: false,
|
|
||||||
obj_change: false,
|
|
||||||
queue_build: false,
|
|
||||||
queue_reload: false,
|
|
||||||
project_config_info: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppConfig {
|
impl AppState {
|
||||||
pub fn set_project_dir(&mut self, path: PathBuf) {
|
pub fn set_project_dir(&mut self, path: PathBuf) {
|
||||||
self.recent_projects.retain(|p| p != &path);
|
self.config.recent_projects.retain(|p| p != &path);
|
||||||
if self.recent_projects.len() > 9 {
|
if self.config.recent_projects.len() > 9 {
|
||||||
self.recent_projects.truncate(9);
|
self.config.recent_projects.truncate(9);
|
||||||
}
|
}
|
||||||
self.recent_projects.insert(0, path.clone());
|
self.config.recent_projects.insert(0, path.clone());
|
||||||
self.project_dir = Some(path);
|
self.config.project_dir = Some(path);
|
||||||
self.target_obj_dir = None;
|
self.config.target_obj_dir = None;
|
||||||
self.base_obj_dir = None;
|
self.config.base_obj_dir = None;
|
||||||
self.selected_obj = None;
|
self.config.selected_obj = None;
|
||||||
self.build_target = false;
|
self.config.build_target = false;
|
||||||
self.objects.clear();
|
self.objects.clear();
|
||||||
self.object_nodes.clear();
|
self.object_nodes.clear();
|
||||||
self.watcher_change = true;
|
self.watcher_change = true;
|
||||||
@@ -187,33 +195,33 @@ impl AppConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_target_obj_dir(&mut self, path: PathBuf) {
|
pub fn set_target_obj_dir(&mut self, path: PathBuf) {
|
||||||
self.target_obj_dir = Some(path);
|
self.config.target_obj_dir = Some(path);
|
||||||
self.selected_obj = None;
|
self.config.selected_obj = None;
|
||||||
self.obj_change = true;
|
self.obj_change = true;
|
||||||
self.queue_build = false;
|
self.queue_build = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_base_obj_dir(&mut self, path: PathBuf) {
|
pub fn set_base_obj_dir(&mut self, path: PathBuf) {
|
||||||
self.base_obj_dir = Some(path);
|
self.config.base_obj_dir = Some(path);
|
||||||
self.selected_obj = None;
|
self.config.selected_obj = None;
|
||||||
self.obj_change = true;
|
self.obj_change = true;
|
||||||
self.queue_build = false;
|
self.queue_build = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_selected_obj(&mut self, object: ObjectConfig) {
|
pub fn set_selected_obj(&mut self, object: ObjectConfig) {
|
||||||
self.selected_obj = Some(object);
|
self.config.selected_obj = Some(object);
|
||||||
self.obj_change = true;
|
self.obj_change = true;
|
||||||
self.queue_build = false;
|
self.queue_build = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type AppConfigRef = Arc<RwLock<AppConfig>>;
|
pub type AppStateRef = Arc<RwLock<AppState>>;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct App {
|
pub struct App {
|
||||||
appearance: Appearance,
|
appearance: Appearance,
|
||||||
view_state: ViewState,
|
view_state: ViewState,
|
||||||
config: AppConfigRef,
|
state: AppStateRef,
|
||||||
modified: Arc<AtomicBool>,
|
modified: Arc<AtomicBool>,
|
||||||
watcher: Option<notify::RecommendedWatcher>,
|
watcher: Option<notify::RecommendedWatcher>,
|
||||||
app_path: Option<PathBuf>,
|
app_path: Option<PathBuf>,
|
||||||
@@ -241,16 +249,17 @@ impl App {
|
|||||||
if let Some(appearance) = eframe::get_value::<Appearance>(storage, APPEARANCE_KEY) {
|
if let Some(appearance) = eframe::get_value::<Appearance>(storage, APPEARANCE_KEY) {
|
||||||
app.appearance = appearance;
|
app.appearance = appearance;
|
||||||
}
|
}
|
||||||
if let Some(mut config) = deserialize_config(storage) {
|
if let Some(config) = deserialize_config(storage) {
|
||||||
if config.project_dir.is_some() {
|
let mut state = AppState { config, ..Default::default() };
|
||||||
config.config_change = true;
|
if state.config.project_dir.is_some() {
|
||||||
config.watcher_change = true;
|
state.config_change = true;
|
||||||
|
state.watcher_change = true;
|
||||||
}
|
}
|
||||||
if config.selected_obj.is_some() {
|
if state.config.selected_obj.is_some() {
|
||||||
config.queue_build = true;
|
state.queue_build = true;
|
||||||
}
|
}
|
||||||
app.view_state.config_state.queue_check_update = config.auto_update_check;
|
app.view_state.config_state.queue_check_update = state.config.auto_update_check;
|
||||||
app.config = Arc::new(RwLock::new(config));
|
app.state = Arc::new(RwLock::new(state));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
app.appearance.init_fonts(&cc.egui_ctx);
|
app.appearance.init_fonts(&cc.egui_ctx);
|
||||||
@@ -336,8 +345,8 @@ impl App {
|
|||||||
jobs.results.append(&mut results);
|
jobs.results.append(&mut results);
|
||||||
jobs.clear_finished();
|
jobs.clear_finished();
|
||||||
|
|
||||||
diff_state.pre_update(jobs, &self.config);
|
diff_state.pre_update(jobs, &self.state);
|
||||||
config_state.pre_update(jobs, &self.config);
|
config_state.pre_update(jobs, &self.state);
|
||||||
debug_assert!(jobs.results.is_empty());
|
debug_assert!(jobs.results.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,23 +354,23 @@ impl App {
|
|||||||
self.appearance.post_update(ctx);
|
self.appearance.post_update(ctx);
|
||||||
|
|
||||||
let ViewState { jobs, diff_state, config_state, graphics_state, .. } = &mut self.view_state;
|
let ViewState { jobs, diff_state, config_state, graphics_state, .. } = &mut self.view_state;
|
||||||
config_state.post_update(ctx, jobs, &self.config);
|
config_state.post_update(ctx, jobs, &self.state);
|
||||||
diff_state.post_update(ctx, jobs, &self.config);
|
diff_state.post_update(ctx, jobs, &self.state);
|
||||||
|
|
||||||
let Ok(mut config) = self.config.write() else {
|
let Ok(mut state) = self.state.write() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let config = &mut *config;
|
let state = &mut *state;
|
||||||
|
|
||||||
if let Some(info) = &config.project_config_info {
|
if let Some(info) = &state.project_config_info {
|
||||||
if file_modified(&info.path, info.timestamp) {
|
if file_modified(&info.path, info.timestamp) {
|
||||||
config.config_change = true;
|
state.config_change = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.config_change {
|
if state.config_change {
|
||||||
config.config_change = false;
|
state.config_change = false;
|
||||||
match load_project_config(config) {
|
match load_project_config(state) {
|
||||||
Ok(()) => config_state.load_error = None,
|
Ok(()) => config_state.load_error = None,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to load project config: {e}");
|
log::error!("Failed to load project config: {e}");
|
||||||
@@ -370,47 +379,50 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.watcher_change {
|
if state.watcher_change {
|
||||||
drop(self.watcher.take());
|
drop(self.watcher.take());
|
||||||
|
|
||||||
if let Some(project_dir) = &config.project_dir {
|
if let Some(project_dir) = &state.config.project_dir {
|
||||||
match build_globset(&config.watch_patterns).map_err(anyhow::Error::new).and_then(
|
match build_globset(&state.config.watch_patterns)
|
||||||
|globset| {
|
.map_err(anyhow::Error::new)
|
||||||
|
.and_then(|globset| {
|
||||||
create_watcher(ctx.clone(), self.modified.clone(), project_dir, globset)
|
create_watcher(ctx.clone(), self.modified.clone(), project_dir, globset)
|
||||||
.map_err(anyhow::Error::new)
|
.map_err(anyhow::Error::new)
|
||||||
},
|
}) {
|
||||||
) {
|
|
||||||
Ok(watcher) => self.watcher = Some(watcher),
|
Ok(watcher) => self.watcher = Some(watcher),
|
||||||
Err(e) => log::error!("Failed to create watcher: {e}"),
|
Err(e) => log::error!("Failed to create watcher: {e}"),
|
||||||
}
|
}
|
||||||
config.watcher_change = false;
|
state.watcher_change = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.obj_change {
|
if state.obj_change {
|
||||||
*diff_state = Default::default();
|
*diff_state = Default::default();
|
||||||
if config.selected_obj.is_some() {
|
if state.config.selected_obj.is_some() {
|
||||||
config.queue_build = true;
|
state.queue_build = true;
|
||||||
}
|
}
|
||||||
config.obj_change = false;
|
state.obj_change = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.modified.swap(false, Ordering::Relaxed) && config.rebuild_on_changes {
|
if self.modified.swap(false, Ordering::Relaxed) && state.config.rebuild_on_changes {
|
||||||
config.queue_build = true;
|
state.queue_build = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(result) = &diff_state.build {
|
if let Some(result) = &diff_state.build {
|
||||||
if let Some((obj, _)) = &result.first_obj {
|
if state.last_mod_check.elapsed().as_millis() >= 500 {
|
||||||
if let (Some(path), Some(timestamp)) = (&obj.path, obj.timestamp) {
|
state.last_mod_check = Instant::now();
|
||||||
if file_modified(path, timestamp) {
|
if let Some((obj, _)) = &result.first_obj {
|
||||||
config.queue_reload = true;
|
if let (Some(path), Some(timestamp)) = (&obj.path, obj.timestamp) {
|
||||||
|
if file_modified(path, timestamp) {
|
||||||
|
state.queue_reload = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if let Some((obj, _)) = &result.second_obj {
|
||||||
if let Some((obj, _)) = &result.second_obj {
|
if let (Some(path), Some(timestamp)) = (&obj.path, obj.timestamp) {
|
||||||
if let (Some(path), Some(timestamp)) = (&obj.path, obj.timestamp) {
|
if file_modified(path, timestamp) {
|
||||||
if file_modified(path, timestamp) {
|
state.queue_reload = true;
|
||||||
config.queue_reload = true;
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -418,17 +430,20 @@ impl App {
|
|||||||
|
|
||||||
// Don't clear `queue_build` if a build is running. A file may have been modified during
|
// Don't clear `queue_build` if a build is running. A file may have been modified during
|
||||||
// the build, so we'll start another build after the current one finishes.
|
// the build, so we'll start another build after the current one finishes.
|
||||||
if config.queue_build && config.selected_obj.is_some() && !jobs.is_running(Job::ObjDiff) {
|
if state.queue_build
|
||||||
jobs.push(start_build(ctx, ObjDiffConfig::from_config(config)));
|
&& state.config.selected_obj.is_some()
|
||||||
config.queue_build = false;
|
&& !jobs.is_running(Job::ObjDiff)
|
||||||
config.queue_reload = false;
|
{
|
||||||
} else if config.queue_reload && !jobs.is_running(Job::ObjDiff) {
|
jobs.push(start_build(ctx, ObjDiffConfig::from_config(&state.config)));
|
||||||
let mut diff_config = ObjDiffConfig::from_config(config);
|
state.queue_build = false;
|
||||||
|
state.queue_reload = false;
|
||||||
|
} else if state.queue_reload && !jobs.is_running(Job::ObjDiff) {
|
||||||
|
let mut diff_config = ObjDiffConfig::from_config(&state.config);
|
||||||
// Don't build, just reload the current files
|
// Don't build, just reload the current files
|
||||||
diff_config.build_base = false;
|
diff_config.build_base = false;
|
||||||
diff_config.build_target = false;
|
diff_config.build_target = false;
|
||||||
jobs.push(start_build(ctx, diff_config));
|
jobs.push(start_build(ctx, diff_config));
|
||||||
config.queue_reload = false;
|
state.queue_reload = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if graphics_state.should_relaunch {
|
if graphics_state.should_relaunch {
|
||||||
@@ -453,7 +468,7 @@ impl eframe::App for App {
|
|||||||
|
|
||||||
self.pre_update(ctx);
|
self.pre_update(ctx);
|
||||||
|
|
||||||
let Self { config, appearance, view_state, .. } = self;
|
let Self { state, appearance, view_state, .. } = self;
|
||||||
let ViewState {
|
let ViewState {
|
||||||
jobs,
|
jobs,
|
||||||
config_state,
|
config_state,
|
||||||
@@ -469,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);
|
||||||
@@ -485,8 +501,8 @@ impl eframe::App for App {
|
|||||||
*show_project_config = !*show_project_config;
|
*show_project_config = !*show_project_config;
|
||||||
ui.close_menu();
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
let recent_projects = if let Ok(guard) = config.read() {
|
let recent_projects = if let Ok(guard) = state.read() {
|
||||||
guard.recent_projects.clone()
|
guard.config.recent_projects.clone()
|
||||||
} else {
|
} else {
|
||||||
vec![]
|
vec![]
|
||||||
};
|
};
|
||||||
@@ -495,12 +511,12 @@ impl eframe::App for App {
|
|||||||
} else {
|
} else {
|
||||||
ui.menu_button("Recent Projects…", |ui| {
|
ui.menu_button("Recent Projects…", |ui| {
|
||||||
if ui.button("Clear").clicked() {
|
if ui.button("Clear").clicked() {
|
||||||
config.write().unwrap().recent_projects.clear();
|
state.write().unwrap().config.recent_projects.clear();
|
||||||
};
|
};
|
||||||
ui.separator();
|
ui.separator();
|
||||||
for path in recent_projects {
|
for path in recent_projects {
|
||||||
if ui.button(format!("{}", path.display())).clicked() {
|
if ui.button(format!("{}", path.display())).clicked() {
|
||||||
config.write().unwrap().set_project_dir(path);
|
state.write().unwrap().set_project_dir(path);
|
||||||
ui.close_menu();
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -533,12 +549,12 @@ impl eframe::App for App {
|
|||||||
*show_arch_config = !*show_arch_config;
|
*show_arch_config = !*show_arch_config;
|
||||||
ui.close_menu();
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
let mut config = config.write().unwrap();
|
let mut state = state.write().unwrap();
|
||||||
let response = ui
|
let response = ui
|
||||||
.checkbox(&mut config.rebuild_on_changes, "Rebuild on changes")
|
.checkbox(&mut state.config.rebuild_on_changes, "Rebuild on changes")
|
||||||
.on_hover_text("Automatically re-run the build & diff when files change.");
|
.on_hover_text("Automatically re-run the build & diff when files change.");
|
||||||
if response.changed() {
|
if response.changed() {
|
||||||
config.watcher_change = true;
|
state.watcher_change = true;
|
||||||
};
|
};
|
||||||
ui.add_enabled(
|
ui.add_enabled(
|
||||||
!diff_state.symbol_state.disable_reverse_fn_order,
|
!diff_state.symbol_state.disable_reverse_fn_order,
|
||||||
@@ -554,7 +570,7 @@ impl eframe::App for App {
|
|||||||
);
|
);
|
||||||
if ui
|
if ui
|
||||||
.checkbox(
|
.checkbox(
|
||||||
&mut config.diff_obj_config.relax_reloc_diffs,
|
&mut state.config.diff_obj_config.relax_reloc_diffs,
|
||||||
"Relax relocation diffs",
|
"Relax relocation diffs",
|
||||||
)
|
)
|
||||||
.on_hover_text(
|
.on_hover_text(
|
||||||
@@ -562,28 +578,32 @@ impl eframe::App for App {
|
|||||||
)
|
)
|
||||||
.changed()
|
.changed()
|
||||||
{
|
{
|
||||||
config.queue_reload = true;
|
state.queue_reload = true;
|
||||||
}
|
}
|
||||||
if ui
|
if ui
|
||||||
.checkbox(
|
.checkbox(
|
||||||
&mut config.diff_obj_config.space_between_args,
|
&mut state.config.diff_obj_config.space_between_args,
|
||||||
"Space between args",
|
"Space between args",
|
||||||
)
|
)
|
||||||
.changed()
|
.changed()
|
||||||
{
|
{
|
||||||
config.queue_reload = true;
|
state.queue_reload = true;
|
||||||
}
|
}
|
||||||
if ui
|
if ui
|
||||||
.checkbox(
|
.checkbox(
|
||||||
&mut config.diff_obj_config.combine_data_sections,
|
&mut state.config.diff_obj_config.combine_data_sections,
|
||||||
"Combine data sections",
|
"Combine data sections",
|
||||||
)
|
)
|
||||||
.on_hover_text("Combines data sections with equal names.")
|
.on_hover_text("Combines data sections with equal names.")
|
||||||
.changed()
|
.changed()
|
||||||
{
|
{
|
||||||
config.queue_reload = true;
|
state.queue_reload = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
ui.separator();
|
||||||
|
if jobs_menu_ui(ui, jobs, appearance) {
|
||||||
|
*show_jobs = !*show_jobs;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -603,8 +623,7 @@ impl eframe::App for App {
|
|||||||
} else {
|
} else {
|
||||||
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, config, show_project_config, config_state, appearance);
|
config_ui(ui, state, show_project_config, config_state, appearance);
|
||||||
jobs_ui(ui, jobs, appearance);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -613,21 +632,22 @@ impl eframe::App for App {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
project_window(ctx, config, show_project_config, config_state, appearance);
|
project_window(ctx, state, show_project_config, config_state, appearance);
|
||||||
appearance_window(ctx, show_appearance_config, appearance);
|
appearance_window(ctx, show_appearance_config, appearance);
|
||||||
demangle_window(ctx, show_demangle, demangle_state, appearance);
|
demangle_window(ctx, show_demangle, demangle_state, appearance);
|
||||||
rlwinm_decode_window(ctx, show_rlwinm_decode, rlwinm_decode_state, appearance);
|
rlwinm_decode_window(ctx, show_rlwinm_decode, rlwinm_decode_state, appearance);
|
||||||
arch_config_window(ctx, config, 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Called by the frame work to save state before shutdown.
|
/// Called by the frame work to save state before shutdown.
|
||||||
fn save(&mut self, storage: &mut dyn eframe::Storage) {
|
fn save(&mut self, storage: &mut dyn eframe::Storage) {
|
||||||
if let Ok(config) = self.config.read() {
|
if let Ok(state) = self.state.read() {
|
||||||
eframe::set_value(storage, CONFIG_KEY, &*config);
|
eframe::set_value(storage, CONFIG_KEY, &state.config);
|
||||||
}
|
}
|
||||||
eframe::set_value(storage, APPEARANCE_KEY, &self.appearance);
|
eframe::set_value(storage, APPEARANCE_KEY, &self.appearance);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ impl ObjectConfigV0 {
|
|||||||
reverse_fn_order: self.reverse_fn_order,
|
reverse_fn_order: self.reverse_fn_order,
|
||||||
complete: None,
|
complete: None,
|
||||||
scratch: None,
|
scratch: None,
|
||||||
|
source_path: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use anyhow::Result;
|
|||||||
use globset::Glob;
|
use globset::Glob;
|
||||||
use objdiff_core::config::{try_project_config, ProjectObject, DEFAULT_WATCH_PATTERNS};
|
use objdiff_core::config::{try_project_config, ProjectObject, DEFAULT_WATCH_PATTERNS};
|
||||||
|
|
||||||
use crate::app::AppConfig;
|
use crate::app::AppState;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub enum ProjectObjectNode {
|
pub enum ProjectObjectNode {
|
||||||
@@ -64,30 +64,30 @@ fn build_nodes(
|
|||||||
nodes
|
nodes
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_project_config(config: &mut AppConfig) -> Result<()> {
|
pub fn load_project_config(state: &mut AppState) -> Result<()> {
|
||||||
let Some(project_dir) = &config.project_dir else {
|
let Some(project_dir) = &state.config.project_dir else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
if let Some((result, info)) = try_project_config(project_dir) {
|
if let Some((result, info)) = try_project_config(project_dir) {
|
||||||
let project_config = result?;
|
let project_config = result?;
|
||||||
config.custom_make = project_config.custom_make;
|
state.config.custom_make = project_config.custom_make;
|
||||||
config.custom_args = project_config.custom_args;
|
state.config.custom_args = project_config.custom_args;
|
||||||
config.target_obj_dir = project_config.target_dir.map(|p| project_dir.join(p));
|
state.config.target_obj_dir = project_config.target_dir.map(|p| project_dir.join(p));
|
||||||
config.base_obj_dir = project_config.base_dir.map(|p| project_dir.join(p));
|
state.config.base_obj_dir = project_config.base_dir.map(|p| project_dir.join(p));
|
||||||
config.build_base = project_config.build_base;
|
state.config.build_base = project_config.build_base;
|
||||||
config.build_target = project_config.build_target;
|
state.config.build_target = project_config.build_target;
|
||||||
config.watch_patterns = project_config.watch_patterns.unwrap_or_else(|| {
|
state.config.watch_patterns = project_config.watch_patterns.unwrap_or_else(|| {
|
||||||
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
|
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
|
||||||
});
|
});
|
||||||
config.watcher_change = true;
|
state.watcher_change = true;
|
||||||
config.objects = project_config.objects;
|
state.objects = project_config.objects;
|
||||||
config.object_nodes = build_nodes(
|
state.object_nodes = build_nodes(
|
||||||
&config.objects,
|
&state.objects,
|
||||||
project_dir,
|
project_dir,
|
||||||
config.target_obj_dir.as_deref(),
|
state.config.target_obj_dir.as_deref(),
|
||||||
config.base_obj_dir.as_deref(),
|
state.config.base_obj_dir.as_deref(),
|
||||||
);
|
);
|
||||||
config.project_config_info = Some(info);
|
state.project_config_info = Some(info);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
use std::{
|
use std::{
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
process::Command,
|
process::Command,
|
||||||
str::from_utf8,
|
|
||||||
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},
|
||||||
@@ -91,13 +90,6 @@ pub(crate) fn run_make(config: &BuildConfig, arg: &Path) -> BuildStatus {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
match run_make_cmd(config, cwd, arg) {
|
|
||||||
Ok(status) => status,
|
|
||||||
Err(e) => BuildStatus { success: false, stderr: e.to_string(), ..Default::default() },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run_make_cmd(config: &BuildConfig, cwd: &Path, arg: &Path) -> Result<BuildStatus> {
|
|
||||||
let make = config.custom_make.as_deref().unwrap_or("make");
|
let make = config.custom_make.as_deref().unwrap_or("make");
|
||||||
let make_args = config.custom_args.as_deref().unwrap_or(&[]);
|
let make_args = config.custom_args.as_deref().unwrap_or(&[]);
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
@@ -144,15 +136,23 @@ fn run_make_cmd(config: &BuildConfig, cwd: &Path, arg: &Path) -> Result<BuildSta
|
|||||||
cmdline.push(' ');
|
cmdline.push(' ');
|
||||||
cmdline.push_str(shell_escape::escape(arg.to_string_lossy()).as_ref());
|
cmdline.push_str(shell_escape::escape(arg.to_string_lossy()).as_ref());
|
||||||
}
|
}
|
||||||
let output = command.output().map_err(|e| anyhow!("Failed to execute build: {e}"))?;
|
let output = match command.output() {
|
||||||
let stdout = from_utf8(&output.stdout).context("Failed to process stdout")?;
|
Ok(output) => output,
|
||||||
let stderr = from_utf8(&output.stderr).context("Failed to process stderr")?;
|
Err(e) => {
|
||||||
Ok(BuildStatus {
|
return BuildStatus {
|
||||||
success: output.status.code().unwrap_or(-1) == 0,
|
success: false,
|
||||||
cmdline,
|
cmdline,
|
||||||
stdout: stdout.to_string(),
|
stdout: Default::default(),
|
||||||
stderr: stderr.to_string(),
|
stderr: e.to_string(),
|
||||||
})
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Try from_utf8 first to avoid copying the buffer if it's valid, then fall back to from_utf8_lossy
|
||||||
|
let stdout = String::from_utf8(output.stdout)
|
||||||
|
.unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned());
|
||||||
|
let stderr = String::from_utf8(output.stderr)
|
||||||
|
.unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned());
|
||||||
|
BuildStatus { success: output.status.success(), cmdline, stdout, stderr }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_build(
|
fn run_build(
|
||||||
@@ -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,44 +236,71 @@ 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()),
|
step_idx,
|
||||||
2,
|
total,
|
||||||
total,
|
&cancel,
|
||||||
&cancel,
|
)?;
|
||||||
)?;
|
step_idx += 1;
|
||||||
Some(read::read(target_path, &config.diff_obj_config).with_context(|| {
|
match read::read(target_path, &config.diff_obj_config) {
|
||||||
format!("Failed to read object '{}'", target_path.display())
|
Ok(obj) => Some(obj),
|
||||||
})?)
|
Err(e) => {
|
||||||
|
first_status = BuildStatus {
|
||||||
|
success: false,
|
||||||
|
stdout: format!("Loading object '{}'", target_path.display()),
|
||||||
|
stderr: format!("{:#}", e),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => None,
|
}
|
||||||
};
|
Some(_) => {
|
||||||
|
step_idx += 1;
|
||||||
|
None
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
let second_obj = match &obj_config.base_path {
|
let second_obj = match &obj_config.base_path {
|
||||||
Some(base_path) if second_status.success => {
|
Some(base_path) if second_status.success => {
|
||||||
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)))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,10 @@ fn main() -> ExitCode {
|
|||||||
|
|
||||||
let app_path = std::env::current_exe().ok();
|
let app_path = std::env::current_exe().ok();
|
||||||
let exec_path: Rc<Mutex<Option<PathBuf>>> = Rc::new(Mutex::new(None));
|
let exec_path: Rc<Mutex<Option<PathBuf>>> = Rc::new(Mutex::new(None));
|
||||||
let mut native_options = eframe::NativeOptions::default();
|
let mut native_options = eframe::NativeOptions {
|
||||||
|
viewport: egui::ViewportBuilder::default().with_app_id(APP_NAME),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
match load_icon() {
|
match load_icon() {
|
||||||
Ok(data) => {
|
Ok(data) => {
|
||||||
native_options.viewport.icon = Some(Arc::new(data));
|
native_options.viewport.icon = Some(Arc::new(data));
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
use std::string::FromUtf16Error;
|
use std::string::FromUtf16Error;
|
||||||
use std::{
|
use std::{
|
||||||
mem::take,
|
mem::take,
|
||||||
path::{PathBuf, MAIN_SEPARATOR},
|
path::{Path, PathBuf, MAIN_SEPARATOR},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(all(windows, feature = "wsl"))]
|
#[cfg(all(windows, feature = "wsl"))]
|
||||||
@@ -19,7 +19,7 @@ use objdiff_core::{
|
|||||||
use strum::{EnumMessage, VariantArray};
|
use strum::{EnumMessage, VariantArray};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::{AppConfig, AppConfigRef, ObjectConfig},
|
app::{AppConfig, AppState, AppStateRef, ObjectConfig},
|
||||||
config::ProjectObjectNode,
|
config::ProjectObjectNode,
|
||||||
jobs::{
|
jobs::{
|
||||||
check_update::{start_check_update, CheckUpdateResult},
|
check_update::{start_check_update, CheckUpdateResult},
|
||||||
@@ -54,7 +54,7 @@ pub struct ConfigViewState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ConfigViewState {
|
impl ConfigViewState {
|
||||||
pub fn pre_update(&mut self, jobs: &mut JobQueue, config: &AppConfigRef) {
|
pub fn pre_update(&mut self, jobs: &mut JobQueue, state: &AppStateRef) {
|
||||||
jobs.results.retain_mut(|result| {
|
jobs.results.retain_mut(|result| {
|
||||||
if let JobResult::CheckUpdate(result) = result {
|
if let JobResult::CheckUpdate(result) = result {
|
||||||
self.check_update = take(result);
|
self.check_update = take(result);
|
||||||
@@ -71,21 +71,21 @@ impl ConfigViewState {
|
|||||||
match self.file_dialog_state.poll() {
|
match self.file_dialog_state.poll() {
|
||||||
FileDialogResult::None => {}
|
FileDialogResult::None => {}
|
||||||
FileDialogResult::ProjectDir(path) => {
|
FileDialogResult::ProjectDir(path) => {
|
||||||
let mut guard = config.write().unwrap();
|
let mut guard = state.write().unwrap();
|
||||||
guard.set_project_dir(path.to_path_buf());
|
guard.set_project_dir(path.to_path_buf());
|
||||||
}
|
}
|
||||||
FileDialogResult::TargetDir(path) => {
|
FileDialogResult::TargetDir(path) => {
|
||||||
let mut guard = config.write().unwrap();
|
let mut guard = state.write().unwrap();
|
||||||
guard.set_target_obj_dir(path.to_path_buf());
|
guard.set_target_obj_dir(path.to_path_buf());
|
||||||
}
|
}
|
||||||
FileDialogResult::BaseDir(path) => {
|
FileDialogResult::BaseDir(path) => {
|
||||||
let mut guard = config.write().unwrap();
|
let mut guard = state.write().unwrap();
|
||||||
guard.set_base_obj_dir(path.to_path_buf());
|
guard.set_base_obj_dir(path.to_path_buf());
|
||||||
}
|
}
|
||||||
FileDialogResult::Object(path) => {
|
FileDialogResult::Object(path) => {
|
||||||
let mut guard = config.write().unwrap();
|
let mut guard = state.write().unwrap();
|
||||||
if let (Some(base_dir), Some(target_dir)) =
|
if let (Some(base_dir), Some(target_dir)) =
|
||||||
(&guard.base_obj_dir, &guard.target_obj_dir)
|
(&guard.config.base_obj_dir, &guard.config.target_obj_dir)
|
||||||
{
|
{
|
||||||
if let Ok(obj_path) = path.strip_prefix(base_dir) {
|
if let Ok(obj_path) = path.strip_prefix(base_dir) {
|
||||||
let target_path = target_dir.join(obj_path);
|
let target_path = target_dir.join(obj_path);
|
||||||
@@ -96,6 +96,7 @@ impl ConfigViewState {
|
|||||||
reverse_fn_order: None,
|
reverse_fn_order: None,
|
||||||
complete: None,
|
complete: None,
|
||||||
scratch: None,
|
scratch: None,
|
||||||
|
source_path: None,
|
||||||
});
|
});
|
||||||
} else if let Ok(obj_path) = path.strip_prefix(target_dir) {
|
} else if let Ok(obj_path) = path.strip_prefix(target_dir) {
|
||||||
let base_path = base_dir.join(obj_path);
|
let base_path = base_dir.join(obj_path);
|
||||||
@@ -106,6 +107,7 @@ impl ConfigViewState {
|
|||||||
reverse_fn_order: None,
|
reverse_fn_order: None,
|
||||||
complete: None,
|
complete: None,
|
||||||
scratch: None,
|
scratch: None,
|
||||||
|
source_path: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -113,11 +115,11 @@ impl ConfigViewState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn post_update(&mut self, ctx: &egui::Context, jobs: &mut JobQueue, config: &AppConfigRef) {
|
pub fn post_update(&mut self, ctx: &egui::Context, jobs: &mut JobQueue, state: &AppStateRef) {
|
||||||
if self.queue_build {
|
if self.queue_build {
|
||||||
self.queue_build = false;
|
self.queue_build = false;
|
||||||
if let Ok(mut config) = config.write() {
|
if let Ok(mut state) = state.write() {
|
||||||
config.queue_build = true;
|
state.queue_build = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,42 +169,43 @@ fn fetch_wsl2_distros() -> Vec<String> {
|
|||||||
|
|
||||||
pub fn config_ui(
|
pub fn config_ui(
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
config: &AppConfigRef,
|
state: &AppStateRef,
|
||||||
show_config_window: &mut bool,
|
show_config_window: &mut bool,
|
||||||
state: &mut ConfigViewState,
|
config_state: &mut ConfigViewState,
|
||||||
appearance: &Appearance,
|
appearance: &Appearance,
|
||||||
) {
|
) {
|
||||||
let mut config_guard = config.write().unwrap();
|
let mut state_guard = state.write().unwrap();
|
||||||
let AppConfig {
|
let AppState {
|
||||||
target_obj_dir,
|
config:
|
||||||
base_obj_dir,
|
AppConfig {
|
||||||
selected_obj,
|
project_dir, target_obj_dir, base_obj_dir, selected_obj, auto_update_check, ..
|
||||||
auto_update_check,
|
},
|
||||||
objects,
|
objects,
|
||||||
object_nodes,
|
object_nodes,
|
||||||
..
|
..
|
||||||
} = &mut *config_guard;
|
} = &mut *state_guard;
|
||||||
|
|
||||||
ui.heading("Updates");
|
ui.heading("Updates");
|
||||||
ui.checkbox(auto_update_check, "Check for updates on startup");
|
ui.checkbox(auto_update_check, "Check for updates on startup");
|
||||||
if ui.add_enabled(!state.check_update_running, egui::Button::new("Check now")).clicked() {
|
if ui.add_enabled(!config_state.check_update_running, egui::Button::new("Check now")).clicked()
|
||||||
state.queue_check_update = true;
|
{
|
||||||
|
config_state.queue_check_update = true;
|
||||||
}
|
}
|
||||||
ui.label(format!("Current version: {}", env!("CARGO_PKG_VERSION")));
|
ui.label(format!("Current version: {}", env!("CARGO_PKG_VERSION")));
|
||||||
if let Some(result) = &state.check_update {
|
if let Some(result) = &config_state.check_update {
|
||||||
ui.label(format!("Latest version: {}", result.latest_release.version));
|
ui.label(format!("Latest version: {}", result.latest_release.version));
|
||||||
if result.update_available {
|
if result.update_available {
|
||||||
ui.colored_label(appearance.insert_color, "Update available");
|
ui.colored_label(appearance.insert_color, "Update available");
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
if let Some(bin_name) = &result.found_binary {
|
if let Some(bin_name) = &result.found_binary {
|
||||||
if ui
|
if ui
|
||||||
.add_enabled(!state.update_running, egui::Button::new("Automatic"))
|
.add_enabled(!config_state.update_running, egui::Button::new("Automatic"))
|
||||||
.on_hover_text_at_pointer(
|
.on_hover_text_at_pointer(
|
||||||
"Automatically download and replace the current build",
|
"Automatically download and replace the current build",
|
||||||
)
|
)
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
state.queue_update = Some(bin_name.clone());
|
config_state.queue_update = Some(bin_name.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ui
|
if ui
|
||||||
@@ -231,7 +234,7 @@ pub fn config_ui(
|
|||||||
if objects.is_empty() {
|
if objects.is_empty() {
|
||||||
if let (Some(_base_dir), Some(target_dir)) = (base_obj_dir, target_obj_dir) {
|
if let (Some(_base_dir), Some(target_dir)) = (base_obj_dir, target_obj_dir) {
|
||||||
if ui.button("Select object").clicked() {
|
if ui.button("Select object").clicked() {
|
||||||
state.file_dialog_state.queue(
|
config_state.file_dialog_state.queue(
|
||||||
|| {
|
|| {
|
||||||
Box::pin(
|
Box::pin(
|
||||||
rfd::AsyncFileDialog::new()
|
rfd::AsyncFileDialog::new()
|
||||||
@@ -254,8 +257,8 @@ pub fn config_ui(
|
|||||||
ui.colored_label(appearance.delete_color, "Missing project settings");
|
ui.colored_label(appearance.delete_color, "Missing project settings");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let had_search = !state.object_search.is_empty();
|
let had_search = !config_state.object_search.is_empty();
|
||||||
egui::TextEdit::singleline(&mut state.object_search).hint_text("Filter").ui(ui);
|
egui::TextEdit::singleline(&mut config_state.object_search).hint_text("Filter").ui(ui);
|
||||||
|
|
||||||
let mut root_open = None;
|
let mut root_open = None;
|
||||||
let mut node_open = NodeOpen::Default;
|
let mut node_open = NodeOpen::Default;
|
||||||
@@ -277,19 +280,22 @@ pub fn config_ui(
|
|||||||
node_open = NodeOpen::Object;
|
node_open = NodeOpen::Object;
|
||||||
}
|
}
|
||||||
let mut filters_text = RichText::new("Filter ⏷");
|
let mut filters_text = RichText::new("Filter ⏷");
|
||||||
if state.filter_diffable || state.filter_incomplete || state.show_hidden {
|
if config_state.filter_diffable
|
||||||
|
|| config_state.filter_incomplete
|
||||||
|
|| config_state.show_hidden
|
||||||
|
{
|
||||||
filters_text = filters_text.color(appearance.replace_color);
|
filters_text = filters_text.color(appearance.replace_color);
|
||||||
}
|
}
|
||||||
egui::menu::menu_button(ui, filters_text, |ui| {
|
egui::menu::menu_button(ui, filters_text, |ui| {
|
||||||
ui.checkbox(&mut state.filter_diffable, "Diffable")
|
ui.checkbox(&mut config_state.filter_diffable, "Diffable")
|
||||||
.on_hover_text_at_pointer("Only show objects with a source file");
|
.on_hover_text_at_pointer("Only show objects with a source file");
|
||||||
ui.checkbox(&mut state.filter_incomplete, "Incomplete")
|
ui.checkbox(&mut config_state.filter_incomplete, "Incomplete")
|
||||||
.on_hover_text_at_pointer("Only show objects not marked complete");
|
.on_hover_text_at_pointer("Only show objects not marked complete");
|
||||||
ui.checkbox(&mut state.show_hidden, "Hidden")
|
ui.checkbox(&mut config_state.show_hidden, "Hidden")
|
||||||
.on_hover_text_at_pointer("Show hidden (auto-generated) objects");
|
.on_hover_text_at_pointer("Show hidden (auto-generated) objects");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
if state.object_search.is_empty() {
|
if config_state.object_search.is_empty() {
|
||||||
if had_search {
|
if had_search {
|
||||||
root_open = Some(true);
|
root_open = Some(true);
|
||||||
node_open = NodeOpen::Object;
|
node_open = NodeOpen::Object;
|
||||||
@@ -306,39 +312,45 @@ pub fn config_ui(
|
|||||||
.open(root_open)
|
.open(root_open)
|
||||||
.default_open(true)
|
.default_open(true)
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
let search = state.object_search.to_ascii_lowercase();
|
let search = config_state.object_search.to_ascii_lowercase();
|
||||||
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
|
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
|
||||||
for node in object_nodes.iter().filter_map(|node| {
|
for node in object_nodes.iter().filter_map(|node| {
|
||||||
filter_node(
|
filter_node(
|
||||||
node,
|
node,
|
||||||
&search,
|
&search,
|
||||||
state.filter_diffable,
|
config_state.filter_diffable,
|
||||||
state.filter_incomplete,
|
config_state.filter_incomplete,
|
||||||
state.show_hidden,
|
config_state.show_hidden,
|
||||||
)
|
)
|
||||||
}) {
|
}) {
|
||||||
display_node(ui, &mut new_selected_obj, &node, appearance, node_open);
|
display_node(
|
||||||
|
ui,
|
||||||
|
&mut new_selected_obj,
|
||||||
|
project_dir.as_deref(),
|
||||||
|
&node,
|
||||||
|
appearance,
|
||||||
|
node_open,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if new_selected_obj != *selected_obj {
|
if new_selected_obj != *selected_obj {
|
||||||
if let Some(obj) = new_selected_obj {
|
if let Some(obj) = new_selected_obj {
|
||||||
// Will set obj_changed, which will trigger a rebuild
|
// Will set obj_changed, which will trigger a rebuild
|
||||||
config_guard.set_selected_obj(obj);
|
state_guard.set_selected_obj(obj);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if config_guard.selected_obj.is_some()
|
if state_guard.config.selected_obj.is_some()
|
||||||
&& ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked()
|
&& ui.add_enabled(!config_state.build_running, egui::Button::new("Build")).clicked()
|
||||||
{
|
{
|
||||||
state.queue_build = true;
|
config_state.queue_build = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.separator();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn display_object(
|
fn display_object(
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
selected_obj: &mut Option<ObjectConfig>,
|
selected_obj: &mut Option<ObjectConfig>,
|
||||||
|
project_dir: Option<&Path>,
|
||||||
name: &str,
|
name: &str,
|
||||||
object: &ProjectObject,
|
object: &ProjectObject,
|
||||||
appearance: &Appearance,
|
appearance: &Appearance,
|
||||||
@@ -356,7 +368,7 @@ fn display_object(
|
|||||||
} else {
|
} else {
|
||||||
appearance.text_color
|
appearance.text_color
|
||||||
};
|
};
|
||||||
let clicked = SelectableLabel::new(
|
let response = SelectableLabel::new(
|
||||||
selected,
|
selected,
|
||||||
RichText::new(name)
|
RichText::new(name)
|
||||||
.font(FontId {
|
.font(FontId {
|
||||||
@@ -365,11 +377,13 @@ fn display_object(
|
|||||||
})
|
})
|
||||||
.color(color),
|
.color(color),
|
||||||
)
|
)
|
||||||
.ui(ui)
|
.ui(ui);
|
||||||
.clicked();
|
if get_source_path(project_dir, object).is_some() {
|
||||||
|
response.context_menu(|ui| object_context_ui(ui, object, project_dir));
|
||||||
|
}
|
||||||
// Always recreate ObjectConfig if selected, in case the project config changed.
|
// Always recreate ObjectConfig if selected, in case the project config changed.
|
||||||
// ObjectConfig is compared using equality, so this won't unnecessarily trigger a rebuild.
|
// ObjectConfig is compared using equality, so this won't unnecessarily trigger a rebuild.
|
||||||
if selected || clicked {
|
if selected || response.clicked() {
|
||||||
*selected_obj = Some(ObjectConfig {
|
*selected_obj = Some(ObjectConfig {
|
||||||
name: object_name.to_string(),
|
name: object_name.to_string(),
|
||||||
target_path: object.target_path.clone(),
|
target_path: object.target_path.clone(),
|
||||||
@@ -377,10 +391,31 @@ fn display_object(
|
|||||||
reverse_fn_order: object.reverse_fn_order(),
|
reverse_fn_order: object.reverse_fn_order(),
|
||||||
complete: object.complete(),
|
complete: object.complete(),
|
||||||
scratch: object.scratch.clone(),
|
scratch: object.scratch.clone(),
|
||||||
|
source_path: object.source_path().cloned(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_source_path(project_dir: Option<&Path>, object: &ProjectObject) -> Option<PathBuf> {
|
||||||
|
project_dir.and_then(|dir| object.source_path().map(|path| dir.join(path)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn object_context_ui(ui: &mut egui::Ui, object: &ProjectObject, project_dir: Option<&Path>) {
|
||||||
|
if let Some(source_path) = get_source_path(project_dir, object) {
|
||||||
|
if ui
|
||||||
|
.button("Open source file")
|
||||||
|
.on_hover_text("Open the source file in the default editor")
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
log::info!("Opening file {}", source_path.display());
|
||||||
|
if let Err(e) = open::that_detached(&source_path) {
|
||||||
|
log::error!("Failed to open source file: {e}");
|
||||||
|
}
|
||||||
|
ui.close_menu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default, Copy, Clone, PartialEq, Eq, Debug)]
|
#[derive(Default, Copy, Clone, PartialEq, Eq, Debug)]
|
||||||
enum NodeOpen {
|
enum NodeOpen {
|
||||||
#[default]
|
#[default]
|
||||||
@@ -393,13 +428,14 @@ enum NodeOpen {
|
|||||||
fn display_node(
|
fn display_node(
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
selected_obj: &mut Option<ObjectConfig>,
|
selected_obj: &mut Option<ObjectConfig>,
|
||||||
|
project_dir: Option<&Path>,
|
||||||
node: &ProjectObjectNode,
|
node: &ProjectObjectNode,
|
||||||
appearance: &Appearance,
|
appearance: &Appearance,
|
||||||
node_open: NodeOpen,
|
node_open: NodeOpen,
|
||||||
) {
|
) {
|
||||||
match node {
|
match node {
|
||||||
ProjectObjectNode::File(name, object) => {
|
ProjectObjectNode::File(name, object) => {
|
||||||
display_object(ui, selected_obj, name, object, appearance);
|
display_object(ui, selected_obj, project_dir, name, object, appearance);
|
||||||
}
|
}
|
||||||
ProjectObjectNode::Dir(name, children) => {
|
ProjectObjectNode::Dir(name, children) => {
|
||||||
let contains_obj = selected_obj.as_ref().map(|path| contains_node(node, path));
|
let contains_obj = selected_obj.as_ref().map(|path| contains_node(node, path));
|
||||||
@@ -425,7 +461,7 @@ fn display_node(
|
|||||||
.open(open)
|
.open(open)
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
for node in children {
|
for node in children {
|
||||||
display_node(ui, selected_obj, node, appearance, node_open);
|
display_node(ui, selected_obj, project_dir, node, appearance, node_open);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -523,33 +559,33 @@ fn pick_folder_ui(
|
|||||||
|
|
||||||
pub fn project_window(
|
pub fn project_window(
|
||||||
ctx: &egui::Context,
|
ctx: &egui::Context,
|
||||||
config: &AppConfigRef,
|
state: &AppStateRef,
|
||||||
show: &mut bool,
|
show: &mut bool,
|
||||||
state: &mut ConfigViewState,
|
config_state: &mut ConfigViewState,
|
||||||
appearance: &Appearance,
|
appearance: &Appearance,
|
||||||
) {
|
) {
|
||||||
let mut config_guard = config.write().unwrap();
|
let mut state_guard = state.write().unwrap();
|
||||||
|
|
||||||
egui::Window::new("Project").open(show).show(ctx, |ui| {
|
egui::Window::new("Project").open(show).show(ctx, |ui| {
|
||||||
split_obj_config_ui(ui, &mut config_guard, state, appearance);
|
split_obj_config_ui(ui, &mut state_guard, config_state, appearance);
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(error) = &state.load_error {
|
if let Some(error) = &config_state.load_error {
|
||||||
let mut open = true;
|
let mut open = true;
|
||||||
egui::Window::new("Error").open(&mut open).show(ctx, |ui| {
|
egui::Window::new("Error").open(&mut open).show(ctx, |ui| {
|
||||||
ui.label("Failed to load project config:");
|
ui.label("Failed to load project config:");
|
||||||
ui.colored_label(appearance.delete_color, error);
|
ui.colored_label(appearance.delete_color, error);
|
||||||
});
|
});
|
||||||
if !open {
|
if !open {
|
||||||
state.load_error = None;
|
config_state.load_error = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn split_obj_config_ui(
|
fn split_obj_config_ui(
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
config: &mut AppConfig,
|
state: &mut AppState,
|
||||||
state: &mut ConfigViewState,
|
config_state: &mut ConfigViewState,
|
||||||
appearance: &Appearance,
|
appearance: &Appearance,
|
||||||
) {
|
) {
|
||||||
let text_format = TextFormat::simple(appearance.ui_font.clone(), appearance.text_color);
|
let text_format = TextFormat::simple(appearance.ui_font.clone(), appearance.text_color);
|
||||||
@@ -560,7 +596,7 @@ fn split_obj_config_ui(
|
|||||||
|
|
||||||
let response = pick_folder_ui(
|
let response = pick_folder_ui(
|
||||||
ui,
|
ui,
|
||||||
&config.project_dir,
|
&state.config.project_dir,
|
||||||
"Project directory",
|
"Project directory",
|
||||||
|ui| {
|
|ui| {
|
||||||
let mut job = LayoutJob::default();
|
let mut job = LayoutJob::default();
|
||||||
@@ -576,7 +612,7 @@ fn split_obj_config_ui(
|
|||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
if response.clicked() {
|
if response.clicked() {
|
||||||
state.file_dialog_state.queue(
|
config_state.file_dialog_state.queue(
|
||||||
|| Box::pin(rfd::AsyncFileDialog::new().pick_folder()),
|
|| Box::pin(rfd::AsyncFileDialog::new().pick_folder()),
|
||||||
FileDialogResult::ProjectDir,
|
FileDialogResult::ProjectDir,
|
||||||
);
|
);
|
||||||
@@ -605,33 +641,35 @@ fn split_obj_config_ui(
|
|||||||
ui.label(job);
|
ui.label(job);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
let mut custom_make_str = config.custom_make.clone().unwrap_or_default();
|
let mut custom_make_str = state.config.custom_make.clone().unwrap_or_default();
|
||||||
if ui
|
if ui
|
||||||
.add_enabled(
|
.add_enabled(
|
||||||
config.project_config_info.is_none(),
|
state.project_config_info.is_none(),
|
||||||
egui::TextEdit::singleline(&mut custom_make_str).hint_text("make"),
|
egui::TextEdit::singleline(&mut custom_make_str).hint_text("make"),
|
||||||
)
|
)
|
||||||
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
|
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
|
||||||
.changed()
|
.changed()
|
||||||
{
|
{
|
||||||
if custom_make_str.is_empty() {
|
if custom_make_str.is_empty() {
|
||||||
config.custom_make = None;
|
state.config.custom_make = None;
|
||||||
} else {
|
} else {
|
||||||
config.custom_make = Some(custom_make_str);
|
state.config.custom_make = Some(custom_make_str);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[cfg(all(windows, feature = "wsl"))]
|
#[cfg(all(windows, feature = "wsl"))]
|
||||||
{
|
{
|
||||||
if state.available_wsl_distros.is_none() {
|
if config_state.available_wsl_distros.is_none() {
|
||||||
state.available_wsl_distros = Some(fetch_wsl2_distros());
|
config_state.available_wsl_distros = Some(fetch_wsl2_distros());
|
||||||
}
|
}
|
||||||
egui::ComboBox::from_label("Run in WSL2")
|
egui::ComboBox::from_label("Run in WSL2")
|
||||||
.selected_text(config.selected_wsl_distro.as_ref().unwrap_or(&"Disabled".to_string()))
|
.selected_text(
|
||||||
|
state.config.selected_wsl_distro.as_ref().unwrap_or(&"Disabled".to_string()),
|
||||||
|
)
|
||||||
.show_ui(ui, |ui| {
|
.show_ui(ui, |ui| {
|
||||||
ui.selectable_value(&mut config.selected_wsl_distro, None, "Disabled");
|
ui.selectable_value(&mut state.config.selected_wsl_distro, None, "Disabled");
|
||||||
for distro in state.available_wsl_distros.as_ref().unwrap() {
|
for distro in config_state.available_wsl_distros.as_ref().unwrap() {
|
||||||
ui.selectable_value(
|
ui.selectable_value(
|
||||||
&mut config.selected_wsl_distro,
|
&mut state.config.selected_wsl_distro,
|
||||||
Some(distro.clone()),
|
Some(distro.clone()),
|
||||||
distro,
|
distro,
|
||||||
);
|
);
|
||||||
@@ -640,10 +678,10 @@ fn split_obj_config_ui(
|
|||||||
}
|
}
|
||||||
ui.separator();
|
ui.separator();
|
||||||
|
|
||||||
if let Some(project_dir) = config.project_dir.clone() {
|
if let Some(project_dir) = state.config.project_dir.clone() {
|
||||||
let response = pick_folder_ui(
|
let response = pick_folder_ui(
|
||||||
ui,
|
ui,
|
||||||
&config.target_obj_dir,
|
&state.config.target_obj_dir,
|
||||||
"Target build directory",
|
"Target build directory",
|
||||||
|ui| {
|
|ui| {
|
||||||
let mut job = LayoutJob::default();
|
let mut job = LayoutJob::default();
|
||||||
@@ -660,17 +698,17 @@ fn split_obj_config_ui(
|
|||||||
ui.label(job);
|
ui.label(job);
|
||||||
},
|
},
|
||||||
appearance,
|
appearance,
|
||||||
config.project_config_info.is_none(),
|
state.project_config_info.is_none(),
|
||||||
);
|
);
|
||||||
if response.clicked() {
|
if response.clicked() {
|
||||||
state.file_dialog_state.queue(
|
config_state.file_dialog_state.queue(
|
||||||
|| Box::pin(rfd::AsyncFileDialog::new().set_directory(&project_dir).pick_folder()),
|
|| Box::pin(rfd::AsyncFileDialog::new().set_directory(&project_dir).pick_folder()),
|
||||||
FileDialogResult::TargetDir,
|
FileDialogResult::TargetDir,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
ui.add_enabled(
|
ui.add_enabled(
|
||||||
config.project_config_info.is_none(),
|
state.project_config_info.is_none(),
|
||||||
egui::Checkbox::new(&mut config.build_target, "Build target objects"),
|
egui::Checkbox::new(&mut state.config.build_target, "Build target objects"),
|
||||||
)
|
)
|
||||||
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
|
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
|
||||||
.on_hover_ui(|ui| {
|
.on_hover_ui(|ui| {
|
||||||
@@ -704,7 +742,7 @@ fn split_obj_config_ui(
|
|||||||
|
|
||||||
let response = pick_folder_ui(
|
let response = pick_folder_ui(
|
||||||
ui,
|
ui,
|
||||||
&config.base_obj_dir,
|
&state.config.base_obj_dir,
|
||||||
"Base build directory",
|
"Base build directory",
|
||||||
|ui| {
|
|ui| {
|
||||||
let mut job = LayoutJob::default();
|
let mut job = LayoutJob::default();
|
||||||
@@ -716,17 +754,17 @@ fn split_obj_config_ui(
|
|||||||
ui.label(job);
|
ui.label(job);
|
||||||
},
|
},
|
||||||
appearance,
|
appearance,
|
||||||
config.project_config_info.is_none(),
|
state.project_config_info.is_none(),
|
||||||
);
|
);
|
||||||
if response.clicked() {
|
if response.clicked() {
|
||||||
state.file_dialog_state.queue(
|
config_state.file_dialog_state.queue(
|
||||||
|| Box::pin(rfd::AsyncFileDialog::new().set_directory(&project_dir).pick_folder()),
|
|| Box::pin(rfd::AsyncFileDialog::new().set_directory(&project_dir).pick_folder()),
|
||||||
FileDialogResult::BaseDir,
|
FileDialogResult::BaseDir,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
ui.add_enabled(
|
ui.add_enabled(
|
||||||
config.project_config_info.is_none(),
|
state.project_config_info.is_none(),
|
||||||
egui::Checkbox::new(&mut config.build_base, "Build base objects"),
|
egui::Checkbox::new(&mut state.config.build_base, "Build base objects"),
|
||||||
)
|
)
|
||||||
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
|
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
|
||||||
.on_hover_ui(|ui| {
|
.on_hover_ui(|ui| {
|
||||||
@@ -757,7 +795,7 @@ fn split_obj_config_ui(
|
|||||||
|
|
||||||
subheading(ui, "Watch settings", appearance);
|
subheading(ui, "Watch settings", appearance);
|
||||||
let response =
|
let response =
|
||||||
ui.checkbox(&mut config.rebuild_on_changes, "Rebuild on changes").on_hover_ui(|ui| {
|
ui.checkbox(&mut state.config.rebuild_on_changes, "Rebuild on changes").on_hover_ui(|ui| {
|
||||||
let mut job = LayoutJob::default();
|
let mut job = LayoutJob::default();
|
||||||
job.append(
|
job.append(
|
||||||
"Automatically re-run the build & diff when files change.",
|
"Automatically re-run the build & diff when files change.",
|
||||||
@@ -767,23 +805,23 @@ fn split_obj_config_ui(
|
|||||||
ui.label(job);
|
ui.label(job);
|
||||||
});
|
});
|
||||||
if response.changed() {
|
if response.changed() {
|
||||||
config.watcher_change = true;
|
state.watcher_change = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label(RichText::new("File patterns").color(appearance.text_color));
|
ui.label(RichText::new("File patterns").color(appearance.text_color));
|
||||||
if ui
|
if ui
|
||||||
.add_enabled(config.project_config_info.is_none(), egui::Button::new("Reset"))
|
.add_enabled(state.project_config_info.is_none(), egui::Button::new("Reset"))
|
||||||
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
|
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
config.watch_patterns =
|
state.config.watch_patterns =
|
||||||
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect();
|
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect();
|
||||||
config.watcher_change = true;
|
state.watcher_change = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
let mut remove_at: Option<usize> = None;
|
let mut remove_at: Option<usize> = None;
|
||||||
for (idx, glob) in config.watch_patterns.iter().enumerate() {
|
for (idx, glob) in state.config.watch_patterns.iter().enumerate() {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label(
|
ui.label(
|
||||||
RichText::new(format!("{}", glob))
|
RichText::new(format!("{}", glob))
|
||||||
@@ -791,7 +829,7 @@ fn split_obj_config_ui(
|
|||||||
.family(FontFamily::Monospace),
|
.family(FontFamily::Monospace),
|
||||||
);
|
);
|
||||||
if ui
|
if ui
|
||||||
.add_enabled(config.project_config_info.is_none(), egui::Button::new("-").small())
|
.add_enabled(state.project_config_info.is_none(), egui::Button::new("-").small())
|
||||||
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
|
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
@@ -800,24 +838,24 @@ fn split_obj_config_ui(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if let Some(idx) = remove_at {
|
if let Some(idx) = remove_at {
|
||||||
config.watch_patterns.remove(idx);
|
state.config.watch_patterns.remove(idx);
|
||||||
config.watcher_change = true;
|
state.watcher_change = true;
|
||||||
}
|
}
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.add_enabled(
|
ui.add_enabled(
|
||||||
config.project_config_info.is_none(),
|
state.project_config_info.is_none(),
|
||||||
egui::TextEdit::singleline(&mut state.watch_pattern_text).desired_width(100.0),
|
egui::TextEdit::singleline(&mut config_state.watch_pattern_text).desired_width(100.0),
|
||||||
)
|
)
|
||||||
.on_disabled_hover_text(CONFIG_DISABLED_TEXT);
|
.on_disabled_hover_text(CONFIG_DISABLED_TEXT);
|
||||||
if ui
|
if ui
|
||||||
.add_enabled(config.project_config_info.is_none(), egui::Button::new("+").small())
|
.add_enabled(state.project_config_info.is_none(), egui::Button::new("+").small())
|
||||||
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
|
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
if let Ok(glob) = Glob::new(&state.watch_pattern_text) {
|
if let Ok(glob) = Glob::new(&config_state.watch_pattern_text) {
|
||||||
config.watch_patterns.push(glob);
|
state.config.watch_patterns.push(glob);
|
||||||
config.watcher_change = true;
|
state.watcher_change = true;
|
||||||
state.watch_pattern_text.clear();
|
config_state.watch_pattern_text.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -825,131 +863,131 @@ fn split_obj_config_ui(
|
|||||||
|
|
||||||
pub fn arch_config_window(
|
pub fn arch_config_window(
|
||||||
ctx: &egui::Context,
|
ctx: &egui::Context,
|
||||||
config: &AppConfigRef,
|
state: &AppStateRef,
|
||||||
show: &mut bool,
|
show: &mut bool,
|
||||||
appearance: &Appearance,
|
appearance: &Appearance,
|
||||||
) {
|
) {
|
||||||
let mut config_guard = config.write().unwrap();
|
let mut state_guard = state.write().unwrap();
|
||||||
egui::Window::new("Arch Settings").open(show).show(ctx, |ui| {
|
egui::Window::new("Arch Settings").open(show).show(ctx, |ui| {
|
||||||
arch_config_ui(ui, &mut config_guard, appearance);
|
arch_config_ui(ui, &mut state_guard, appearance);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn arch_config_ui(ui: &mut egui::Ui, config: &mut AppConfig, _appearance: &Appearance) {
|
fn arch_config_ui(ui: &mut egui::Ui, state: &mut AppState, _appearance: &Appearance) {
|
||||||
ui.heading("x86");
|
ui.heading("x86");
|
||||||
egui::ComboBox::new("x86_formatter", "Format")
|
egui::ComboBox::new("x86_formatter", "Format")
|
||||||
.selected_text(config.diff_obj_config.x86_formatter.get_message().unwrap())
|
.selected_text(state.config.diff_obj_config.x86_formatter.get_message().unwrap())
|
||||||
.show_ui(ui, |ui| {
|
.show_ui(ui, |ui| {
|
||||||
for &formatter in X86Formatter::VARIANTS {
|
for &formatter in X86Formatter::VARIANTS {
|
||||||
if ui
|
if ui
|
||||||
.selectable_label(
|
.selectable_label(
|
||||||
config.diff_obj_config.x86_formatter == formatter,
|
state.config.diff_obj_config.x86_formatter == formatter,
|
||||||
formatter.get_message().unwrap(),
|
formatter.get_message().unwrap(),
|
||||||
)
|
)
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
config.diff_obj_config.x86_formatter = formatter;
|
state.config.diff_obj_config.x86_formatter = formatter;
|
||||||
config.queue_reload = true;
|
state.queue_reload = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
ui.separator();
|
ui.separator();
|
||||||
ui.heading("MIPS");
|
ui.heading("MIPS");
|
||||||
egui::ComboBox::new("mips_abi", "ABI")
|
egui::ComboBox::new("mips_abi", "ABI")
|
||||||
.selected_text(config.diff_obj_config.mips_abi.get_message().unwrap())
|
.selected_text(state.config.diff_obj_config.mips_abi.get_message().unwrap())
|
||||||
.show_ui(ui, |ui| {
|
.show_ui(ui, |ui| {
|
||||||
for &abi in MipsAbi::VARIANTS {
|
for &abi in MipsAbi::VARIANTS {
|
||||||
if ui
|
if ui
|
||||||
.selectable_label(
|
.selectable_label(
|
||||||
config.diff_obj_config.mips_abi == abi,
|
state.config.diff_obj_config.mips_abi == abi,
|
||||||
abi.get_message().unwrap(),
|
abi.get_message().unwrap(),
|
||||||
)
|
)
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
config.diff_obj_config.mips_abi = abi;
|
state.config.diff_obj_config.mips_abi = abi;
|
||||||
config.queue_reload = true;
|
state.queue_reload = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
egui::ComboBox::new("mips_instr_category", "Instruction Category")
|
egui::ComboBox::new("mips_instr_category", "Instruction Category")
|
||||||
.selected_text(config.diff_obj_config.mips_instr_category.get_message().unwrap())
|
.selected_text(state.config.diff_obj_config.mips_instr_category.get_message().unwrap())
|
||||||
.show_ui(ui, |ui| {
|
.show_ui(ui, |ui| {
|
||||||
for &category in MipsInstrCategory::VARIANTS {
|
for &category in MipsInstrCategory::VARIANTS {
|
||||||
if ui
|
if ui
|
||||||
.selectable_label(
|
.selectable_label(
|
||||||
config.diff_obj_config.mips_instr_category == category,
|
state.config.diff_obj_config.mips_instr_category == category,
|
||||||
category.get_message().unwrap(),
|
category.get_message().unwrap(),
|
||||||
)
|
)
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
config.diff_obj_config.mips_instr_category = category;
|
state.config.diff_obj_config.mips_instr_category = category;
|
||||||
config.queue_reload = true;
|
state.queue_reload = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
ui.separator();
|
ui.separator();
|
||||||
ui.heading("ARM");
|
ui.heading("ARM");
|
||||||
egui::ComboBox::new("arm_arch_version", "Architecture Version")
|
egui::ComboBox::new("arm_arch_version", "Architecture Version")
|
||||||
.selected_text(config.diff_obj_config.arm_arch_version.get_message().unwrap())
|
.selected_text(state.config.diff_obj_config.arm_arch_version.get_message().unwrap())
|
||||||
.show_ui(ui, |ui| {
|
.show_ui(ui, |ui| {
|
||||||
for &version in ArmArchVersion::VARIANTS {
|
for &version in ArmArchVersion::VARIANTS {
|
||||||
if ui
|
if ui
|
||||||
.selectable_label(
|
.selectable_label(
|
||||||
config.diff_obj_config.arm_arch_version == version,
|
state.config.diff_obj_config.arm_arch_version == version,
|
||||||
version.get_message().unwrap(),
|
version.get_message().unwrap(),
|
||||||
)
|
)
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
config.diff_obj_config.arm_arch_version = version;
|
state.config.diff_obj_config.arm_arch_version = version;
|
||||||
config.queue_reload = true;
|
state.queue_reload = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
let response = ui
|
let response = ui
|
||||||
.checkbox(&mut config.diff_obj_config.arm_unified_syntax, "Unified syntax")
|
.checkbox(&mut state.config.diff_obj_config.arm_unified_syntax, "Unified syntax")
|
||||||
.on_hover_text("Disassemble as unified assembly language (UAL).");
|
.on_hover_text("Disassemble as unified assembly language (UAL).");
|
||||||
if response.changed() {
|
if response.changed() {
|
||||||
config.queue_reload = true;
|
state.queue_reload = true;
|
||||||
}
|
}
|
||||||
let response = ui
|
let response = ui
|
||||||
.checkbox(&mut config.diff_obj_config.arm_av_registers, "Use A/V registers")
|
.checkbox(&mut state.config.diff_obj_config.arm_av_registers, "Use A/V registers")
|
||||||
.on_hover_text("Display R0-R3 as A1-A4 and R4-R11 as V1-V8");
|
.on_hover_text("Display R0-R3 as A1-A4 and R4-R11 as V1-V8");
|
||||||
if response.changed() {
|
if response.changed() {
|
||||||
config.queue_reload = true;
|
state.queue_reload = true;
|
||||||
}
|
}
|
||||||
egui::ComboBox::new("arm_r9_usage", "Display R9 as")
|
egui::ComboBox::new("arm_r9_usage", "Display R9 as")
|
||||||
.selected_text(config.diff_obj_config.arm_r9_usage.get_message().unwrap())
|
.selected_text(state.config.diff_obj_config.arm_r9_usage.get_message().unwrap())
|
||||||
.show_ui(ui, |ui| {
|
.show_ui(ui, |ui| {
|
||||||
for &usage in ArmR9Usage::VARIANTS {
|
for &usage in ArmR9Usage::VARIANTS {
|
||||||
if ui
|
if ui
|
||||||
.selectable_label(
|
.selectable_label(
|
||||||
config.diff_obj_config.arm_r9_usage == usage,
|
state.config.diff_obj_config.arm_r9_usage == usage,
|
||||||
usage.get_message().unwrap(),
|
usage.get_message().unwrap(),
|
||||||
)
|
)
|
||||||
.on_hover_text(usage.get_detailed_message().unwrap())
|
.on_hover_text(usage.get_detailed_message().unwrap())
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
config.diff_obj_config.arm_r9_usage = usage;
|
state.config.diff_obj_config.arm_r9_usage = usage;
|
||||||
config.queue_reload = true;
|
state.queue_reload = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
let response = ui
|
let response = ui
|
||||||
.checkbox(&mut config.diff_obj_config.arm_sl_usage, "Display R10 as SL")
|
.checkbox(&mut state.config.diff_obj_config.arm_sl_usage, "Display R10 as SL")
|
||||||
.on_hover_text("Used for explicit stack limits.");
|
.on_hover_text("Used for explicit stack limits.");
|
||||||
if response.changed() {
|
if response.changed() {
|
||||||
config.queue_reload = true;
|
state.queue_reload = true;
|
||||||
}
|
}
|
||||||
let response = ui
|
let response = ui
|
||||||
.checkbox(&mut config.diff_obj_config.arm_fp_usage, "Display R11 as FP")
|
.checkbox(&mut state.config.diff_obj_config.arm_fp_usage, "Display R11 as FP")
|
||||||
.on_hover_text("Used for frame pointers.");
|
.on_hover_text("Used for frame pointers.");
|
||||||
if response.changed() {
|
if response.changed() {
|
||||||
config.queue_reload = true;
|
state.queue_reload = true;
|
||||||
}
|
}
|
||||||
let response = ui
|
let response = ui
|
||||||
.checkbox(&mut config.diff_obj_config.arm_ip_usage, "Display R12 as IP")
|
.checkbox(&mut state.config.diff_obj_config.arm_ip_usage, "Display R12 as IP")
|
||||||
.on_hover_text("Used for interworking and long branches.");
|
.on_hover_text("Used for interworking and long branches.");
|
||||||
if response.changed() {
|
if response.changed() {
|
||||||
config.queue_reload = true;
|
state.queue_reload = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -509,6 +509,18 @@ pub fn function_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
ui.separator();
|
||||||
|
if ui
|
||||||
|
.add_enabled(
|
||||||
|
state.source_path_available,
|
||||||
|
egui::Button::new("🖹 Source file"),
|
||||||
|
)
|
||||||
|
.on_hover_text_at_pointer("Open the source file in the default editor")
|
||||||
|
.on_disabled_hover_text("Source file metadata missing")
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
state.queue_open_source_path = true;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.scope(|ui| {
|
ui.scope(|ui| {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use std::{
|
use std::{
|
||||||
fs::File,
|
fs::File,
|
||||||
|
io::{BufReader, BufWriter},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -46,13 +47,13 @@ pub fn load_graphics_config(path: &Path) -> Result<Option<GraphicsConfig>> {
|
|||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
let file = File::open(path)?;
|
let file = BufReader::new(File::open(path)?);
|
||||||
let config: GraphicsConfig = ron::de::from_reader(file)?;
|
let config: GraphicsConfig = ron::de::from_reader(file)?;
|
||||||
Ok(Some(config))
|
Ok(Some(config))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn save_graphics_config(path: &Path, config: &GraphicsConfig) -> Result<()> {
|
pub fn save_graphics_config(path: &Path, config: &GraphicsConfig) -> Result<()> {
|
||||||
let file = File::create(path)?;
|
let file = BufWriter::new(File::create(path)?);
|
||||||
ron::ser::to_writer(file, config)?;
|
ron::ser::to_writer(file, config)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,58 +1,162 @@
|
|||||||
|
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.horizontal(|ui| {
|
ui.separator();
|
||||||
ui.label(&status.title);
|
ui.horizontal(|ui| {
|
||||||
if ui.small_button("✖").clicked() {
|
ui.label(&status.title);
|
||||||
if job.handle.is_some() {
|
if ui.small_button("✖").clicked() {
|
||||||
job.should_remove = true;
|
if job.handle.is_some() {
|
||||||
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:?}");
|
||||||
}
|
|
||||||
} else {
|
|
||||||
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));
|
|
||||||
} else {
|
|
||||||
ui.label(if status.status.len() > STATUS_LENGTH - 3 {
|
|
||||||
format!("{}…", &status.status[0..STATUS_LENGTH - 3])
|
|
||||||
} else {
|
} else {
|
||||||
format!("{:width$}", &status.status, width = STATUS_LENGTH)
|
remove_job = Some(job.id);
|
||||||
})
|
}
|
||||||
.on_hover_text_at_pointer(&status.status);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
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 {
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use objdiff_core::{
|
|||||||
use regex::{Regex, RegexBuilder};
|
use regex::{Regex, RegexBuilder};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::AppConfigRef,
|
app::AppStateRef,
|
||||||
jobs::{
|
jobs::{
|
||||||
create_scratch::{start_create_scratch, CreateScratchConfig, CreateScratchResult},
|
create_scratch::{start_create_scratch, CreateScratchConfig, CreateScratchResult},
|
||||||
objdiff::{BuildStatus, ObjDiffResult},
|
objdiff::{BuildStatus, ObjDiffResult},
|
||||||
@@ -52,6 +52,8 @@ pub struct DiffViewState {
|
|||||||
pub scratch_available: bool,
|
pub scratch_available: bool,
|
||||||
pub queue_scratch: bool,
|
pub queue_scratch: bool,
|
||||||
pub scratch_running: bool,
|
pub scratch_running: bool,
|
||||||
|
pub source_path_available: bool,
|
||||||
|
pub queue_open_source_path: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
@@ -64,7 +66,7 @@ pub struct SymbolViewState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DiffViewState {
|
impl DiffViewState {
|
||||||
pub fn pre_update(&mut self, jobs: &mut JobQueue, config: &AppConfigRef) {
|
pub fn pre_update(&mut self, jobs: &mut JobQueue, state: &AppStateRef) {
|
||||||
jobs.results.retain_mut(|result| match result {
|
jobs.results.retain_mut(|result| match result {
|
||||||
JobResult::ObjDiff(result) => {
|
JobResult::ObjDiff(result) => {
|
||||||
self.build = take(result);
|
self.build = take(result);
|
||||||
@@ -80,26 +82,29 @@ impl DiffViewState {
|
|||||||
self.scratch_running = jobs.is_running(Job::CreateScratch);
|
self.scratch_running = jobs.is_running(Job::CreateScratch);
|
||||||
|
|
||||||
self.symbol_state.disable_reverse_fn_order = false;
|
self.symbol_state.disable_reverse_fn_order = false;
|
||||||
if let Ok(config) = config.read() {
|
if let Ok(state) = state.read() {
|
||||||
if let Some(obj_config) = &config.selected_obj {
|
if let Some(obj_config) = &state.config.selected_obj {
|
||||||
if let Some(value) = obj_config.reverse_fn_order {
|
if let Some(value) = obj_config.reverse_fn_order {
|
||||||
self.symbol_state.reverse_fn_order = value;
|
self.symbol_state.reverse_fn_order = value;
|
||||||
self.symbol_state.disable_reverse_fn_order = true;
|
self.symbol_state.disable_reverse_fn_order = true;
|
||||||
}
|
}
|
||||||
|
self.source_path_available = obj_config.source_path.is_some();
|
||||||
|
} else {
|
||||||
|
self.source_path_available = false;
|
||||||
}
|
}
|
||||||
self.scratch_available = CreateScratchConfig::is_available(&config);
|
self.scratch_available = CreateScratchConfig::is_available(&state.config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn post_update(&mut self, ctx: &egui::Context, jobs: &mut JobQueue, config: &AppConfigRef) {
|
pub fn post_update(&mut self, ctx: &egui::Context, jobs: &mut JobQueue, state: &AppStateRef) {
|
||||||
if let Some(result) = take(&mut self.scratch) {
|
if let Some(result) = take(&mut self.scratch) {
|
||||||
ctx.output_mut(|o| o.open_url = Some(OpenUrl::new_tab(result.scratch_url)));
|
ctx.output_mut(|o| o.open_url = Some(OpenUrl::new_tab(result.scratch_url)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.queue_build {
|
if self.queue_build {
|
||||||
self.queue_build = false;
|
self.queue_build = false;
|
||||||
if let Ok(mut config) = config.write() {
|
if let Ok(mut state) = state.write() {
|
||||||
config.queue_build = true;
|
state.queue_build = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,8 +113,8 @@ impl DiffViewState {
|
|||||||
if let Some(function_name) =
|
if let Some(function_name) =
|
||||||
self.symbol_state.selected_symbol.as_ref().map(|sym| sym.symbol_name.clone())
|
self.symbol_state.selected_symbol.as_ref().map(|sym| sym.symbol_name.clone())
|
||||||
{
|
{
|
||||||
if let Ok(config) = config.read() {
|
if let Ok(state) = state.read() {
|
||||||
match CreateScratchConfig::from_config(&config, function_name) {
|
match CreateScratchConfig::from_config(&state.config, function_name) {
|
||||||
Ok(config) => {
|
Ok(config) => {
|
||||||
jobs.push_once(Job::CreateScratch, || {
|
jobs.push_once(Job::CreateScratch, || {
|
||||||
start_create_scratch(ctx, config)
|
start_create_scratch(ctx, config)
|
||||||
@@ -122,6 +127,22 @@ impl DiffViewState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.queue_open_source_path {
|
||||||
|
self.queue_open_source_path = false;
|
||||||
|
if let Ok(state) = state.read() {
|
||||||
|
if let (Some(project_dir), Some(source_path)) = (
|
||||||
|
&state.config.project_dir,
|
||||||
|
state.config.selected_obj.as_ref().and_then(|obj| obj.source_path.as_ref()),
|
||||||
|
) {
|
||||||
|
let source_path = project_dir.join(source_path);
|
||||||
|
log::info!("Opening file {}", source_path.display());
|
||||||
|
open::that_detached(source_path).unwrap_or_else(|err| {
|
||||||
|
log::error!("Failed to open source file: {err}");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -431,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() {
|
||||||
@@ -444,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);
|
||||||
|
|
||||||
ui.label(&status.cmdline);
|
if !status.cmdline.is_empty() {
|
||||||
ui.colored_label(appearance.replace_color, &status.stdout);
|
ui.label(&status.cmdline);
|
||||||
ui.colored_label(appearance.delete_color, &status.stderr);
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -518,10 +545,27 @@ pub fn symbol_diff_ui(ui: &mut Ui, state: &mut DiffViewState, appearance: &Appea
|
|||||||
|ui| {
|
|ui| {
|
||||||
ui.set_width(column_width);
|
ui.set_width(column_width);
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.scope(|ui| {
|
||||||
|
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||||
|
ui.label("Build base:");
|
||||||
|
});
|
||||||
|
ui.separator();
|
||||||
|
if ui
|
||||||
|
.add_enabled(
|
||||||
|
state.source_path_available,
|
||||||
|
egui::Button::new("🖹 Source file"),
|
||||||
|
)
|
||||||
|
.on_hover_text_at_pointer("Open the source file in the default editor")
|
||||||
|
.on_disabled_hover_text("Source file metadata missing")
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
state.queue_open_source_path = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
ui.scope(|ui| {
|
ui.scope(|ui| {
|
||||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||||
|
|
||||||
ui.label("Build base:");
|
|
||||||
if result.second_status.success {
|
if result.second_status.success {
|
||||||
if result.second_obj.is_none() {
|
if result.second_obj.is_none() {
|
||||||
ui.colored_label(appearance.replace_color, "Missing");
|
ui.colored_label(appearance.replace_color, "Missing");
|
||||||
|
|||||||
Reference in New Issue
Block a user