use super::picker::{PickKind, pick_file};
use crate::{
MainWindow,
gui::{
library::local::{ForeignFormat, convert_foreign_design, converted_asc_file_name},
show_toast,
},
};
use indicatrix_cut_core::{
NativeDesignFile,
native::{
DesignLoadError, FileKind, LoadNativeOnlyResult, LoadedDesign, design_from_str,
load_native_only,
},
native_path_for_asc,
};
use indicatrix_formats::native::design::{DesignFileError, detect_kind};
use std::path::{Path, PathBuf};
pub(super) enum PickedNative {
Pair(Box<PickedPair>),
Design {
native_path: PathBuf,
loaded: Box<LoadedDesign>,
},
AscOnly { asc_path: PathBuf, asc_text: String },
SelfContained {
native_path: PathBuf,
loaded: Box<LoadNativeOnlyResult>,
asc_filename: String,
},
Converted(ConvertedPick),
}
pub(super) struct ConvertedPick {
pub(super) source_path: PathBuf,
pub(super) asc_file_name: String,
pub(super) asc_text: String,
pub(super) warnings: Vec<String>,
}
pub(super) fn read_foreign_design(path: &Path) -> Result<ConvertedPick, String> {
let format = ForeignFormat::from_path(path)
.ok_or_else(|| format!("'{}' is not a .gem or .gcs file.", path.display()))?;
let bytes =
std::fs::read(path).map_err(|e| format!("Failed to read {}: {e}", path.display()))?;
let converted =
crate::gui::library::local::catch_file_panic(std::panic::AssertUnwindSafe(|| {
convert_foreign_design(format, &bytes)
}))
.map_err(|panic_msg| {
format!(
"Cannot open {}: internal error: {panic_msg}",
path.display()
)
})?
.map_err(|e| format!("Cannot open {}: {e}", path.display()))?;
let file_name = path.file_name().map_or_else(
|| format!("design{}", format.dotted_extension()),
|n| n.to_string_lossy().into_owned(),
);
Ok(ConvertedPick {
source_path: path.to_path_buf(),
asc_file_name: converted_asc_file_name(&file_name),
asc_text: converted.asc_text,
warnings: converted.warnings,
})
}
pub(super) struct PickedPair {
pub(super) native_path: PathBuf,
pub(super) parsed_native: Box<NativeDesignFile>,
pub(super) asc_text: String,
pub(super) native_text: String,
}
pub(super) enum ReadOutcome {
Design(Box<LoadedDesign>),
Pair {
parsed_native: Box<NativeDesignFile>,
asc_text: String,
native_text: String,
},
SelfContained {
loaded: Box<LoadNativeOnlyResult>,
asc_filename: String,
},
}
pub(super) fn read_native_pair_then(
ui: &MainWindow,
native_path: &Path,
on_done: impl FnOnce(&MainWindow, Option<ReadOutcome>) + 'static,
) {
let native_text = match std::fs::read_to_string(native_path) {
Ok(text) => text,
Err(e) => {
show_toast(
ui,
&format!("Failed to read {}: {e}", native_path.display()),
"error",
);
on_done(ui, None);
return;
}
};
if detect_kind(native_text.as_bytes()) == FileKind::Design {
match design_from_str(&native_text) {
Ok(loaded) => on_done(ui, Some(ReadOutcome::Design(Box::new(loaded)))),
Err(e) => {
show_toast(ui, &design_open_error_message(native_path, &e), "error");
on_done(ui, None);
}
}
return;
}
let parsed_native = match indicatrix_cut_core::native::parse_toml_string(&native_text) {
Ok(n) => Box::new(n),
Err(e) => {
show_toast(
ui,
&format!(
"'{}' is not a valid .indicatrix.toml sidecar: {e}",
native_path.display()
),
"error",
);
on_done(ui, None);
return;
}
};
let asc_path = native_path.with_file_name(&parsed_native.asc_filename);
let asc_filename = parsed_native.asc_filename.clone();
let asc_findable = asc_path.is_file()
|| indicatrix_cut_core::asc_path_for_native(native_path)
.is_some_and(|guessed| guessed != asc_path && guessed.is_file());
if !asc_findable && let Ok(loaded) = load_native_only(&native_text) {
on_done(
ui,
Some(ReadOutcome::SelfContained {
loaded: Box::new(loaded),
asc_filename,
}),
);
return;
}
resolve_paired_asc_text_then(
ui,
native_path,
&asc_path,
&asc_filename,
move |ui, asc_text| {
on_done(
ui,
asc_text.map(|asc_text| ReadOutcome::Pair {
parsed_native,
asc_text,
native_text,
}),
);
},
);
}
pub(super) fn design_open_error_message(path: &Path, error: &DesignLoadError) -> String {
match error {
DesignLoadError::File(DesignFileError::UnsupportedVersion { found, supported }) => {
format!(
"Cannot open '{}': it was saved by a newer version of Indicatrix (file \
version {found}; this version reads up to {supported}). Update Indicatrix \
to open it.",
path.display()
)
}
other => format!("Cannot open '{}': {other}", path.display()),
}
}
fn read_asc_text(path: &Path) -> std::io::Result<String> {
std::fs::read(path).map(|bytes| indicatrix_formats::asc::decode_asc_bytes(&bytes).into_owned())
}
fn resolve_paired_asc_text_then(
ui: &MainWindow,
native_path: &Path,
recorded_asc_path: &Path,
recorded_asc_filename: &str,
on_done: impl FnOnce(&MainWindow, Option<String>) + 'static,
) {
if let Ok(text) = read_asc_text(recorded_asc_path) {
on_done(ui, Some(text));
return;
}
if let Some(guessed) = indicatrix_cut_core::asc_path_for_native(native_path)
&& guessed != recorded_asc_path
&& let Ok(text) = read_asc_text(&guessed)
{
on_done(ui, Some(text));
return;
}
show_toast(
ui,
&format!(
"'{}' names a paired .asc file '{recorded_asc_filename}', but it could not be found \
next to it. Locate it to continue.",
native_path.display()
),
"info",
);
pick_file(ui, PickKind::LocateAsc, move |ui, located| {
let Some(located) = located else {
on_done(ui, None);
return;
};
match read_asc_text(&located) {
Ok(text) => on_done(ui, Some(text)),
Err(e) => {
show_toast(
ui,
&format!("Failed to read {}: {e}", located.display()),
"error",
);
on_done(ui, None);
}
}
});
}
fn picked_native_from_pair_result(native_path: PathBuf, outcome: ReadOutcome) -> PickedNative {
match outcome {
ReadOutcome::Design(loaded) => PickedNative::Design {
native_path,
loaded,
},
ReadOutcome::Pair {
parsed_native,
asc_text,
native_text,
} => PickedNative::Pair(Box::new(PickedPair {
native_path,
parsed_native,
asc_text,
native_text,
})),
ReadOutcome::SelfContained {
loaded,
asc_filename,
} => PickedNative::SelfContained {
native_path,
loaded,
asc_filename,
},
}
}
fn read_picked_asc_then(
ui: &MainWindow,
asc_path: PathBuf,
on_done: impl FnOnce(&MainWindow, Option<PickedNative>) + 'static,
) {
let native_path = native_path_for_asc(&asc_path);
if native_path.is_file() {
let native_path_for_read = native_path.clone();
read_native_pair_then(ui, &native_path_for_read, move |ui, result| {
on_done(
ui,
result.map(|outcome| picked_native_from_pair_result(native_path, outcome)),
);
});
return;
}
let asc_text = match read_asc_text(&asc_path) {
Ok(text) => text,
Err(e) => {
show_toast(
ui,
&format!("Failed to read {}: {e}", asc_path.display()),
"error",
);
on_done(ui, None);
return;
}
};
on_done(ui, Some(PickedNative::AscOnly { asc_path, asc_text }));
}
pub(super) fn pick_design_then(
ui: &MainWindow,
on_done: impl FnOnce(&MainWindow, Option<PickedNative>) + 'static,
) {
pick_file(ui, PickKind::OpenDesign, move |ui, picked_path| {
let Some(picked_path) = picked_path else {
on_done(ui, None);
return;
};
read_picked_path_then(ui, picked_path, on_done);
});
}
pub(super) fn read_picked_path_then(
ui: &MainWindow,
picked_path: PathBuf,
on_done: impl FnOnce(&MainWindow, Option<PickedNative>) + 'static,
) {
if picked_path
.extension()
.is_some_and(|e| e.eq_ignore_ascii_case("asc"))
{
read_picked_asc_then(ui, picked_path, on_done);
return;
}
if ForeignFormat::from_path(&picked_path).is_some() {
match read_foreign_design(&picked_path) {
Ok(converted) => on_done(ui, Some(PickedNative::Converted(converted))),
Err(message) => {
show_toast(ui, &message, "error");
on_done(ui, None);
}
}
return;
}
let picked_path_for_read = picked_path.clone();
read_native_pair_then(ui, &picked_path_for_read, move |ui, result| {
on_done(
ui,
result.map(|outcome| picked_native_from_pair_result(picked_path, outcome)),
);
});
}