Files
svld/svldgui/src/main.rs
T
2026-05-23 19:05:48 +02:00

369 lines
11 KiB
Rust

#![cfg_attr(all(windows, not(debug_assertions)), windows_subsystem = "windows")]
use std::{
cell::RefCell,
fs,
path::PathBuf,
rc::Rc,
thread,
};
use can_dbc::Dbc;
use chrono::Local;
use slint::{ComponentHandle, ModelRc, StandardListViewItem, VecModel};
slint::include_modules!();
#[derive(Default)]
struct State {
dbc_path: Option<PathBuf>,
asc_paths: Vec<PathBuf>,
output_dir: Option<PathBuf>,
}
struct FileResult {
file_name: String,
date: String,
unknown_count: i32,
error_count: i32,
unknown_ids: String,
error_rows: Vec<(f64, u8, u8)>,
runtime_stats: String,
}
struct Run {
label: String,
files: Vec<FileResult>,
}
#[derive(Default)]
struct Results {
runs: Vec<Run>,
window: Option<ResultsWindow>,
run_counter: usize,
}
thread_local! {
static RESULTS: RefCell<Results> = RefCell::new(Results::default());
}
fn build_file_result(
path: &PathBuf,
info: libsvld::DecodeProcessInfo,
) -> FileResult {
let file_name = path
.file_name()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| path.display().to_string());
let date = info
.date
.and_utc()
.to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
let unknown_count = info.unknown_ids.len() as i32;
let error_count = info.error_frames.len() as i32;
let mut unknown_sorted: Vec<u32> = info.unknown_ids.iter().copied().collect();
unknown_sorted.sort();
let unknown_ids = if unknown_sorted.is_empty() {
"(none)".to_string()
} else {
unknown_sorted
.iter()
.map(|id| format!("0x{:X}", id))
.collect::<Vec<_>>()
.join(", ")
};
let error_rows: Vec<(f64, u8, u8)> = info
.error_frames
.iter()
.map(|f| (f.timestamp, f.channel, f.data))
.collect();
let runtime_stats = format!(
"threads: {} · page size: {} · pages/chunk: {} · chunk size: {}",
info.thread_count, info.page_size, info.pages_per_chunk, info.chunk_size
);
FileResult {
file_name,
date,
unknown_count,
error_count,
unknown_ids,
error_rows,
runtime_stats,
}
}
fn rebuild_entries(results: &Results) -> Vec<ResultEntry> {
let last_idx = results.runs.len().saturating_sub(1);
let mut entries = Vec::new();
for (run_idx, run) in results.runs.iter().enumerate() {
let is_new = run_idx == last_idx;
entries.push(ResultEntry {
is_header: true,
is_new,
label: run.label.clone().into(),
});
for f in &run.files {
entries.push(ResultEntry {
is_header: false,
is_new: false,
label: f.file_name.clone().into(),
});
}
}
entries
}
fn find_file(results: &Results, target: usize) -> Option<&FileResult> {
let mut i = 0;
for run in &results.runs {
if i == target {
return None;
}
i += 1;
for f in &run.files {
if i == target {
return Some(f);
}
i += 1;
}
}
None
}
fn apply_selection(window: &ResultsWindow, results: &Results, idx: i32) {
if idx < 0 {
window.set_selected_index(-1);
return;
}
let Some(f) = find_file(results, idx as usize) else {
return;
};
window.set_selected_index(idx);
window.set_sel_label(f.file_name.clone().into());
window.set_sel_date(f.date.clone().into());
window.set_sel_unknown_count(f.unknown_count);
window.set_sel_error_count(f.error_count);
window.set_sel_unknown_ids(f.unknown_ids.clone().into());
window.set_sel_runtime_stats(f.runtime_stats.clone().into());
let rows: Vec<ModelRc<StandardListViewItem>> = f
.error_rows
.iter()
.map(|(t, ch, d)| {
let cells: Vec<StandardListViewItem> = vec![
slint::format!("{}", t).into(),
slint::format!("{}", ch).into(),
slint::format!("0b{:08b}", d).into(),
];
ModelRc::new(VecModel::from(cells))
})
.collect();
window.set_sel_error_rows(ModelRc::new(VecModel::from(rows)));
}
fn show_results(results: &mut Results, new_run: Run) {
results.runs.push(new_run);
let entries = rebuild_entries(results);
let new_first_file_idx = entries
.iter()
.enumerate()
.rev()
.find(|(_, e)| !e.is_header)
.map(|(i, _)| i as i32)
.unwrap_or(-1);
let new_first_file_idx = {
// We want the first non-header entry of the last run, not the very last.
let last_header_idx = entries
.iter()
.enumerate()
.rev()
.find(|(_, e)| e.is_header)
.map(|(i, _)| i)
.unwrap_or(0);
entries
.iter()
.enumerate()
.skip(last_header_idx + 1)
.find(|(_, e)| !e.is_header)
.map(|(i, _)| i as i32)
.unwrap_or(new_first_file_idx)
};
if results.window.is_none() {
let win = ResultsWindow::new().unwrap();
let win_weak = win.as_weak();
win.on_select(move |i| {
if let Some(w) = win_weak.upgrade() {
RESULTS.with(|cell| {
apply_selection(&w, &cell.borrow(), i);
});
}
});
results.window = Some(win);
}
let win = results.window.as_ref().unwrap();
let model = ModelRc::new(VecModel::from(entries));
win.set_entries(model);
apply_selection(win, results, new_first_file_idx);
win.show().unwrap();
}
fn main() {
let window = MainWindow::new().unwrap();
let state = Rc::new(RefCell::new(State::default()));
window.on_open_results(|| {
RESULTS.with(|cell| {
if let Some(w) = cell.borrow().window.as_ref() {
w.show().ok();
}
});
});
window.on_browse_dbc({
let window_weak = window.as_weak();
let state = state.clone();
move || {
if let Some(path) = rfd::FileDialog::new()
.set_title("Select DBC")
.add_filter("DBC files", &["dbc"])
.pick_file()
{
let display = path.display().to_string();
state.borrow_mut().dbc_path = Some(path);
if let Some(w) = window_weak.upgrade() {
w.set_dbc_path(display.into());
}
}
}
});
window.on_browse_asc({
let window_weak = window.as_weak();
let state = state.clone();
move || {
if let Some(paths) = rfd::FileDialog::new()
.set_title("Select ASC file(s)")
.add_filter("ASC files", &["asc"])
.pick_files()
{
let label = match paths.len() {
0 => String::new(),
1 => paths[0].display().to_string(),
n => format!("{} file(s) selected", n),
};
let has_asc = !paths.is_empty();
state.borrow_mut().asc_paths = paths;
if let Some(w) = window_weak.upgrade() {
w.set_asc_label(label.into());
w.set_has_asc(has_asc);
}
}
}
});
window.on_browse_output({
let window_weak = window.as_weak();
let state = state.clone();
move || {
if let Some(path) = rfd::FileDialog::new()
.set_title("Select output directory")
.pick_folder()
{
let display = path.display().to_string();
state.borrow_mut().output_dir = Some(path);
if let Some(w) = window_weak.upgrade() {
w.set_output_dir(display.into());
}
}
}
});
window.on_decode({
let window_weak = window.as_weak();
let state = state.clone();
move || {
let s = state.borrow();
let (Some(dbc_path), Some(output_dir)) = (s.dbc_path.clone(), s.output_dir.clone())
else {
return;
};
let asc_paths = s.asc_paths.clone();
if asc_paths.is_empty() {
return;
}
drop(s);
let total = asc_paths.len();
if let Some(w) = window_weak.upgrade() {
w.set_busy(true);
w.set_status(format!("Decoding... 0/{}", total).into());
}
let window_weak = window_weak.clone();
thread::spawn(move || {
let result = (|| -> Result<Vec<FileResult>, String> {
let data = fs::read_to_string(&dbc_path)
.map_err(|e| format!("Failed to read DBC: {}", e))?;
let dbc = Dbc::try_from(data.as_str())
.map_err(|e| format!("Failed to parse DBC: {:?}", e))?;
let mut out = Vec::with_capacity(total);
for (i, asc) in asc_paths.iter().enumerate() {
let info = libsvld::decode_to_motec(&dbc, asc, &output_dir);
out.push(build_file_result(asc, info));
let done = i + 1;
let w = window_weak.clone();
slint::invoke_from_event_loop(move || {
if let Some(w) = w.upgrade() {
w.set_status(format!("Decoding... {}/{}", done, total).into());
}
})
.ok();
}
Ok(out)
})();
slint::invoke_from_event_loop(move || {
if let Some(w) = window_weak.upgrade() {
w.set_busy(false);
}
match result {
Ok(files) => {
if let Some(w) = window_weak.upgrade() {
w.set_status("Done.".into());
w.set_has_results(true);
}
RESULTS.with(|cell| {
let mut r = cell.borrow_mut();
r.run_counter += 1;
let label = format!(
"Run {}{}",
r.run_counter,
Local::now().format("%H:%M:%S")
);
show_results(&mut r, Run { label, files });
});
}
Err(e) => {
if let Some(w) = window_weak.upgrade() {
w.set_status(format!("Error: {}", e).into());
}
}
}
})
.ok();
});
}
});
window.run().unwrap();
}