kr580 1.0.0

Desktop KR580VM80 / Intel 8080 emulator.
Documentation
use crate::app::{DesktopApp, StatusKind};
use crate::backend::AppCommand;
use crate::i18n::Key;
use crate::settings_storage::{load_settings, save_settings};
use std::path::{Path, PathBuf};

impl DesktopApp {
    pub(crate) fn open_floppy_image(&mut self) {
        let settings = load_settings();
        let mut dialog =
            rfd::FileDialog::new().add_filter("KR580 floppy image", &["kpd", "img", "bin"]);

        let preferred = self
            .snapshot
            .devices
            .floppy
            .path
            .as_ref()
            .or(settings.general.floppy_image_path.as_ref())
            .unwrap_or(&settings.storage.floppy_path);
        if let Some(parent) = preferred
            .parent()
            .filter(|parent| !parent.as_os_str().is_empty())
        {
            dialog = dialog.set_directory(parent);
        }
        if let Some(name) = preferred.file_name() {
            dialog = dialog.set_file_name(name.to_string_lossy().as_ref());
        }

        let Some(path) = dialog.pick_file() else {
            return;
        };

        self.clear_error_notice();
        self.dispatch_sync(AppCommand::AttachFloppyImage(path.clone()));
        if self.error_notice.is_some() {
            return;
        }

        let mut settings = settings;
        settings.storage.floppy_path = path.clone();
        save_settings(&settings);
        self.refresh_hdd_file_exists();
        self.set_status(StatusKind::FloppyImageAttached {
            display: path.display().to_string(),
        });
        if self.floppy_show_image_contents {
            self.refresh_floppy_image_contents();
        }
    }

    pub(crate) fn save_floppy_buffer(&mut self) {
        let settings = load_settings();
        let mut dialog = rfd::FileDialog::new().set_file_name("floppy_buffer.kpd");
        for (name, extensions) in floppy_buffer_save_filters() {
            dialog = dialog.add_filter(name, extensions);
        }

        let preferred = self
            .snapshot
            .devices
            .floppy
            .path
            .as_ref()
            .unwrap_or(&settings.storage.floppy_path);
        if let Some(parent) = preferred
            .parent()
            .filter(|parent| !parent.as_os_str().is_empty())
        {
            dialog = dialog.set_directory(parent);
        }

        let Some(path) = dialog.save_file() else {
            return;
        };

        match save_floppy_buffer_file(&path, &self.snapshot.devices.floppy.visible_buffer) {
            Ok(path) => self.set_status_custom(format!(
                "{}: {}",
                self.lang.t(Key::FloppyBufferSaved),
                path.display()
            )),
            Err(error) => {
                tracing::error!("save floppy buffer to {}: {error}", path.display());
                self.set_status_custom(self.lang.t(Key::ErrCannotWriteFile).to_owned());
            }
        }
    }

    pub(crate) fn refresh_floppy_image_contents(&mut self) {
        let Some(path) = self.snapshot.devices.floppy.path.as_ref() else {
            self.floppy_image_contents.clear();
            self.floppy_image_error = Some(self.lang.t(Key::FloppyPathMissing).into());
            return;
        };

        match read_floppy_image_contents(path) {
            Ok(bytes) => {
                self.floppy_image_contents = bytes;
                self.floppy_image_error = None;
            }
            Err(error) => {
                self.floppy_image_contents.clear();
                self.floppy_image_error =
                    Some(format!("{}: {error}", self.lang.t(Key::ErrCannotReadFile)));
            }
        }
    }
}

fn read_floppy_image_contents(path: &Path) -> std::io::Result<Vec<u8>> {
    std::fs::read(path)
}

fn floppy_buffer_save_filters() -> [(&'static str, &'static [&'static str]); 3] {
    [
        ("KR580 floppy buffer (*.kpd)", &["kpd"]),
        ("KR580 floppy image (*.img)", &["img"]),
        ("Raw binary buffer (*.bin)", &["bin"]),
    ]
}

fn save_floppy_buffer_file(path: &Path, bytes: &[u8]) -> std::io::Result<PathBuf> {
    let path = floppy_buffer_save_path(path);
    std::fs::write(&path, bytes)?;
    Ok(path)
}

pub(crate) fn hdd_default_path() -> PathBuf {
    let settings = load_settings();
    let dir = settings.general.hdd_directory.unwrap_or_else(|| {
        std::env::var("HOME")
            .or_else(|_| std::env::var("USERPROFILE"))
            .map(PathBuf::from)
            .unwrap_or_else(|_| PathBuf::from("."))
    });
    dir.join("hdd.kpd")
}

fn floppy_buffer_save_path(path: &Path) -> PathBuf {
    match path
        .extension()
        .and_then(|ext| ext.to_str())
        .map(str::to_ascii_lowercase)
        .as_deref()
    {
        Some("kpd" | "img" | "bin") => path.to_path_buf(),
        _ => {
            let mut raw = path.as_os_str().to_os_string();
            raw.push(".kpd");
            PathBuf::from(raw)
        }
    }
}
impl DesktopApp {
    pub(crate) fn choose_hdd_directory(&mut self) {
        let mut dialog = rfd::FileDialog::new();

        let preferred = self
            .snapshot
            .devices
            .hdd
            .path
            .as_ref()
            .cloned()
            .unwrap_or_else(hdd_default_path);
        if let Some(parent) = preferred
            .parent()
            .filter(|parent| !parent.as_os_str().is_empty())
        {
            dialog = dialog.set_directory(parent);
        }

        let Some(folder) = dialog.pick_folder() else {
            return;
        };

        self.clear_error_notice();
        let hdd_path = folder.join("hdd.kpd");
        self.hdd_file_exists = true;
        self.dispatch_sync(crate::backend::AppCommand::AttachHddFile(hdd_path.clone()));
        if self.error_notice.is_some() {
            self.hdd_file_exists = false;
            return;
        }

        self.set_status_custom(format!("HDD: {}", hdd_path.display()));
    }
    pub(crate) fn delete_hdd_file(&mut self) {
        let Some(path) = self.snapshot.devices.hdd.path.clone() else {
            return;
        };
        if !path.exists() {
            self.hdd_file_exists = false;
            return;
        }
        if let Err(error) = std::fs::remove_file(&path) {
            tracing::error!("failed to delete HDD file {}: {error}", path.display());
            self.set_status_custom(self.lang.t(Key::ErrCannotWriteFile).to_owned());
            return;
        }
        self.hdd_file_exists = false;
        self.dispatch_sync(crate::backend::AppCommand::DetachHddFile);
        self.set_status_custom(format!(
            "{}: {}",
            self.lang.t(Key::HddFileDeleted),
            path.display()
        ));
    }

    pub(crate) fn create_hdd_file(&mut self) {
        let path = self
            .snapshot
            .devices
            .hdd
            .path
            .clone()
            .unwrap_or_else(hdd_default_path);
        self.dispatch_sync(crate::backend::AppCommand::AttachHddFile(path.clone()));
        if self.error_notice.is_some() {
            return;
        }
        self.hdd_file_exists = true;
        self.set_status_custom(format!("HDD: {}", path.display()));
    }

    pub(crate) fn refresh_hdd_file_exists(&mut self) {
        self.hdd_file_exists = self
            .snapshot
            .devices
            .hdd
            .path
            .as_ref()
            .is_some_and(|p| p.exists());
    }

    pub(crate) fn refresh_hdd_image_contents(&mut self) {
        let Some(path) = self.snapshot.devices.hdd.path.as_ref() else {
            self.hdd_image_contents.clear();
            self.hdd_image_error = Some(self.lang.t(Key::HddPathMissing).into());
            return;
        };

        match std::fs::read(path) {
            Ok(bytes) => {
                self.hdd_image_contents = bytes;
                self.hdd_image_error = None;
            }
            Err(error) => {
                self.hdd_image_contents.clear();
                self.hdd_image_error =
                    Some(format!("{}: {error}", self.lang.t(Key::ErrCannotReadFile)));
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::floppy_buffer_save_filters;
    use super::read_floppy_image_contents;
    use super::save_floppy_buffer_file;
    use std::fs;
    use std::time::{SystemTime, UNIX_EPOCH};

    #[test]
    fn read_floppy_image_contents_returns_file_bytes() {
        let stamp = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_nanos();
        let path = std::env::temp_dir().join(format!("kr580-floppy-image-{stamp}.kpd"));
        fs::write(&path, [b'K', b'R', 0x80]).unwrap();

        let bytes = read_floppy_image_contents(&path).unwrap();

        fs::remove_file(&path).unwrap();
        assert_eq!(bytes, [b'K', b'R', 0x80]);
    }

    #[test]
    fn save_floppy_buffer_file_writes_bytes_and_defaults_to_kpd() {
        let stamp = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_nanos();
        let base = std::env::temp_dir().join(format!("kr580-floppy-buffer-{stamp}"));

        let path = save_floppy_buffer_file(&base, &[b'A', 0x80]).unwrap();

        let bytes = fs::read(&path).unwrap();
        fs::remove_file(&path).unwrap();
        assert_eq!(path.extension().and_then(|ext| ext.to_str()), Some("kpd"));
        assert_eq!(bytes, [b'A', 0x80]);
    }

    #[test]
    fn floppy_buffer_save_filters_are_separate_and_kpd_first() {
        let filters = floppy_buffer_save_filters();

        assert_eq!(
            filters,
            [
                ("KR580 floppy buffer (*.kpd)", &["kpd"][..]),
                ("KR580 floppy image (*.img)", &["img"][..]),
                ("Raw binary buffer (*.bin)", &["bin"][..]),
            ]
        );
    }
}