kr580 1.0.0

Desktop KR580VM80 / Intel 8080 emulator.
Documentation
use crate::devices::{DeviceError, DeviceStatus};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use tokio::io::AsyncWriteExt;
use tokio::sync::mpsc;

mod pdf;

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PrinterState {
    pub spool: Vec<u8>,
    pub target_path: Option<PathBuf>,
    pub status: DeviceStatus,
    pub bytes_buffered: u64,
    pub last_error: Option<String>,
}

#[derive(Debug)]
pub struct PrinterDevice {
    state: PrinterState,
    tx: Option<mpsc::UnboundedSender<Vec<u8>>>,
    completion_tx: mpsc::UnboundedSender<PrintCompletion>,
    completion_rx: mpsc::UnboundedReceiver<PrintCompletion>,
}

#[derive(Debug)]
struct PrintCompletion {
    path: PathBuf,
    result: Result<(), String>,
}

impl Default for PrinterDevice {
    fn default() -> Self {
        let (completion_tx, completion_rx) = mpsc::unbounded_channel();
        Self {
            state: PrinterState {
                spool: Vec::new(),
                target_path: None,
                status: DeviceStatus::Ready,
                bytes_buffered: 0,
                last_error: None,
            },
            tx: None,
            completion_tx,
            completion_rx,
        }
    }
}

impl PrinterDevice {
    pub fn output_byte(&mut self, value: u8) {
        self.state.spool.push(value);
        self.state.bytes_buffered += 1;
    }

    pub fn attach_export_path(&mut self, path: impl AsRef<Path>, handle: &tokio::runtime::Handle) {
        let path = path.as_ref().to_path_buf();
        let (tx, mut rx) = mpsc::unbounded_channel::<Vec<u8>>();
        let worker_path = path.clone();
        handle.spawn(async move {
            while let Some(bytes) = rx.recv().await {
                match tokio::fs::OpenOptions::new()
                    .create(true)
                    .append(true)
                    .open(&worker_path)
                    .await
                {
                    Ok(mut file) => {
                        if let Err(err) = file.write_all(&bytes).await {
                            tracing::error!(error = %err, "printer export write failed");
                            continue;
                        }
                        if let Err(err) = file.flush().await {
                            tracing::error!(error = %err, "printer export flush failed");
                        }
                    }
                    Err(err) => tracing::error!(error = %err, "printer export open failed"),
                }
            }
        });
        self.state.target_path = Some(path);
        self.tx = Some(tx);
    }

    pub fn print_spool(&mut self) -> Result<(), DeviceError> {
        let Some(tx) = self.tx.clone() else {
            self.state.status = DeviceStatus::NotReady;
            self.state.last_error = Some(DeviceError::NotReady.to_string());
            return Err(DeviceError::NotReady);
        };
        tx.send(self.state.spool.clone()).map_err(|_| {
            self.state.status = DeviceStatus::Disconnected;
            self.state.last_error = Some(DeviceError::Disconnected.to_string());
            DeviceError::Disconnected
        })?;
        self.state.last_error = None;
        Ok(())
    }

    pub fn print_to_pdf(
        &mut self,
        path: impl AsRef<Path>,
        handle: &tokio::runtime::Handle,
    ) -> Result<(), DeviceError> {
        if self.state.status == DeviceStatus::Busy {
            return Err(DeviceError::Busy);
        }
        let path = path.as_ref().to_path_buf();
        let spool = self.state.spool.clone();
        let completion_tx = self.completion_tx.clone();
        let completion_path = path.clone();
        self.state.target_path = Some(path);
        self.state.status = DeviceStatus::Busy;
        self.state.last_error = None;
        handle.spawn_blocking(move || {
            let result = pdf::write(&completion_path, &spool);
            let _ = completion_tx.send(PrintCompletion {
                path: completion_path,
                result,
            });
        });
        Ok(())
    }

    pub fn poll(&mut self) -> bool {
        let mut changed = false;
        while let Ok(completion) = self.completion_rx.try_recv() {
            changed = true;
            self.state.target_path = Some(completion.path);
            match completion.result {
                Ok(()) => {
                    self.state.status = DeviceStatus::Ready;
                    self.state.last_error = None;
                }
                Err(error) => {
                    self.state.status = DeviceStatus::Error(error.clone());
                    self.state.last_error = Some(error);
                }
            }
        }
        changed
    }

    pub fn clear(&mut self) {
        self.state.spool.clear();
        self.state.bytes_buffered = 0;
    }

    pub fn input_byte(&self) -> u8 {
        self.state.status.code()
    }

    pub fn state(&self) -> PrinterState {
        self.state.clone()
    }
}