From 2ff94829e540b4187f4d58f4b7f46b794f9af8d1 Mon Sep 17 00:00:00 2001 From: Xiphoseer Date: Sun, 5 Jan 2025 21:55:01 +0100 Subject: [PATCH] feat: work towards CHSET list --- crates/sdo-web/index.html | 7 +- crates/sdo-web/src/glue.rs | 42 ++++++++++++ crates/sdo-web/src/lib.rs | 64 ++++++++++-------- crates/sdo-web/src/vfs.rs | 108 ++++++++++++++++++++---------- crates/signum/src/chsets/cache.rs | 63 ++++++++++++++--- 5 files changed, 209 insertions(+), 75 deletions(-) create mode 100644 crates/sdo-web/src/glue.rs diff --git a/crates/sdo-web/index.html b/crates/sdo-web/index.html index ea8ce06..4f8851f 100644 --- a/crates/sdo-web/index.html +++ b/crates/sdo-web/index.html @@ -32,10 +32,13 @@
Menu
diff --git a/crates/sdo-web/src/glue.rs b/crates/sdo-web/src/glue.rs new file mode 100644 index 0000000..64bb80e --- /dev/null +++ b/crates/sdo-web/src/glue.rs @@ -0,0 +1,42 @@ +use js_sys::{ArrayBuffer, Function, Reflect, Symbol, Uint8Array}; +use wasm_bindgen::{JsCast, JsValue}; +use wasm_bindgen_futures::JsFuture; +use web_sys::FileSystemFileHandle; + +pub(crate) async fn fs_file_handle_get_file( + file_handle: &FileSystemFileHandle, +) -> Result { + let file = JsFuture::from(file_handle.get_file()) + .await? + .unchecked_into::(); + Ok(file) +} + +pub(crate) async fn js_file_data(file: &web_sys::File) -> Result { + let buf = js_file_array_buffer(file).await?; + Ok(Uint8Array::new(&buf)) +} + +pub(crate) async fn js_file_array_buffer(file: &web_sys::File) -> Result { + let array_buffer = JsFuture::from(file.array_buffer()) + .await? + .unchecked_into::(); + Ok(array_buffer) +} + +pub(crate) fn try_iter_async(val: &JsValue) -> Result, JsValue> { + let async_iter_sym = Symbol::async_iterator(); + let iter_fn = Reflect::get(val, async_iter_sym.as_ref())?; + + let iter_fn: Function = match iter_fn.dyn_into() { + Ok(iter_fn) => iter_fn, + Err(_) => return Ok(None), + }; + + let it: js_sys::AsyncIterator = match iter_fn.call0(val)?.dyn_into() { + Ok(it) => it, + Err(_) => return Ok(None), + }; + + Ok(Some(it)) +} diff --git a/crates/sdo-web/src/lib.rs b/crates/sdo-web/src/lib.rs index a5dcc6d..47da9e5 100644 --- a/crates/sdo-web/src/lib.rs +++ b/crates/sdo-web/src/lib.rs @@ -1,13 +1,14 @@ #![allow(non_snake_case)] // wasm_bindgen macro use bstr::BStr; +use glue::js_file_data; use image::ImageOutputFormat; -use js_sys::{Array, ArrayBuffer, JsString, Uint8Array}; +use js_sys::{Array, JsString, Uint8Array}; use log::{info, warn, Level}; use sdo_util::keymap::{KB_DRAW, NP_DRAW}; use signum::{ chsets::{ - cache::{ChsetCache, DocumentFontCacheInfo}, + cache::{AsyncIterator, ChsetCache, DocumentFontCacheInfo, VfsDirEntry, VFS}, editor::{parse_eset, ESet}, encoding::decode_atari_str, printer::parse_ps24, @@ -22,7 +23,7 @@ use signum::{ raster::{self, render_doc_page, render_editor_text, Page}, util::FourCC, }; -use std::{fmt::Write, io::Cursor}; +use std::{ffi::OsStr, fmt::Write, io::Cursor}; use vfs::OriginPrivateFS; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::JsFuture; @@ -32,6 +33,7 @@ use web_sys::{ HtmlCanvasElement, HtmlElement, HtmlImageElement, HtmlInputElement, ImageBitmap, Url, }; +mod glue; mod vfs; /* @@ -94,13 +96,6 @@ fn js_four_cc(arr: &Uint8Array) -> Option { } } -async fn js_file_data(file: &web_sys::File) -> Result { - let buf = JsFuture::from(file.array_buffer()) - .await? - .unchecked_into::(); - Ok(Uint8Array::new(&buf)) -} - pub struct ActiveDocument { sdoc: SDoc<'static>, dfci: DocumentFontCacheInfo, @@ -472,16 +467,9 @@ impl Handle { &self.fc, dfci, ); - //let list_item = self.document.create_element("div")?; - //list_item.class_list().add_1("list-group-item")?; let blob = self.page_as_blob(&page)?; Ok(blob) - //let img = Self::blob_image_el(&blob)?; - //img.class_list().add_1("container-fluid")?; - //list_item.append_child(&img)?; - - //self.output.append_child(&list_item)?; } else { warn!("Missing page {index}"); Err("Missing page in pbuf".into()) @@ -544,6 +532,24 @@ impl Handle { } } else if matches!(fragment, "" | "#" | "#/") { self.on_change().await?; + } else if matches!(fragment, "#/CHSETS/") { + self.list_chsets().await?; + } + Ok(()) + } + + async fn list_chsets(&mut self) -> Result<(), JsValue> { + let mut iter = self.fs.read_dir(OriginPrivateFS::chsets_path()).await?; + while let Some(next) = iter.next().await { + let entry = next?; + if self.fs.is_file_entry(&entry) { + let path = entry.path(); + let name = path.file_name().map(OsStr::to_string_lossy); + let _name = name.as_deref().unwrap_or(""); + + let file = self.fs.open_dir_entry(&entry).await?; + let _data = js_file_data(&file).await?; + } } Ok(()) } @@ -553,25 +559,29 @@ impl Handle { self.reset()?; for file in self.input_files()? { let file = file?; - let arr = JsFuture::from(file.array_buffer()) - .await? - .unchecked_into::(); - self.stage(&file.name(), Uint8Array::new(&arr)).await?; + let arr = js_file_data(&file).await?; + self.stage(&file.name(), arr).await?; } Ok(()) } + fn card(&self, name: &str, four_cc: FourCC, href: &str) -> Result { + let card = self.document.create_element("a")?; + let kind = decode_atari_str(&four_cc); + card.class_list() + .add_3("list-group-item", "list-group-item-action", kind.as_ref())?; + card.set_attribute("href", href)?; + self.card_body(&card, name, four_cc)?; + Ok(card) + } + async fn stage(&mut self, name: &str, arr: Uint8Array) -> Result<(), JsValue> { let data = arr.to_vec(); info!("Parsing file '{}'", name); if let Ok((_, four_cc)) = four_cc(&data) { - let card = self.document.create_element("a")?; - let kind = decode_atari_str(&FourCC::SDOC); - card.class_list() - .add_3("list-group-item", "list-group-item-action", kind.as_ref())?; - card.set_attribute("href", &format!("#/staged/{name}"))?; - self.card_body(&card, name, four_cc)?; + let href = format!("#/staged/{name}"); + let card = self.card(name, four_cc, &href)?; match four_cc { FourCC::SDOC => { let doc = self.parse_sdoc(&data)?; diff --git a/crates/sdo-web/src/vfs.rs b/crates/sdo-web/src/vfs.rs index 5c0afd3..dc60778 100644 --- a/crates/sdo-web/src/vfs.rs +++ b/crates/sdo-web/src/vfs.rs @@ -1,7 +1,8 @@ use core::fmt; -use js_sys::{Function, Object, Reflect, Symbol}; -use signum::chsets::cache::{AsyncIterator, VFS}; +use js_sys::Object; +use signum::chsets::cache::{AsyncIterator, VfsDirEntry, VFS}; use std::{ + borrow::Cow, future::Future, path::{Path, PathBuf}, }; @@ -12,6 +13,8 @@ use web_sys::{ FileSystemHandle, FileSystemHandleKind, StorageManager, }; +use crate::glue::{fs_file_handle_get_file, js_file_data, try_iter_async}; + /// Browser Origin Private File System pub struct OriginPrivateFS { storage: StorageManager, @@ -27,9 +30,13 @@ impl OriginPrivateFS { Ok(root) } + pub fn chsets_path() -> &'static Path { + Path::new("CHSETS") + } + pub async fn chset_dir(&self) -> Result { let root = self.root_dir()?; - let dir = resolve_dir(root, Path::new("CHSETS"), true).await?; + let dir = resolve_dir(root, Self::chsets_path(), true).await?; Ok(dir) } } @@ -37,6 +44,12 @@ impl OriginPrivateFS { #[derive(Debug)] pub struct Error(pub JsValue); +impl From for JsValue { + fn from(value: Error) -> Self { + value.0 + } +} + impl From for Error { fn from(value: js_sys::Error) -> Self { Self(value.into()) @@ -80,7 +93,26 @@ impl AsyncIterator for DirIter { } } -pub struct DirEntry(pub FileSystemHandle, pub PathBuf); +pub struct Directory { + inner: FileSystemDirectoryHandle, + path: PathBuf, +} + +impl Directory { + pub async fn read_dir(&self) -> Result { + let iter = + try_iter_async(&self.inner)?.ok_or_else(|| JsError::new("Not async iterable"))?; + Ok(DirIter(iter, self.path.clone())) + } +} + +pub struct DirEntry(FileSystemHandle, PathBuf); + +impl VfsDirEntry for DirEntry { + fn path(&self) -> std::borrow::Cow<'_, Path> { + Cow::Borrowed(&self.1) + } +} impl fmt::Display for DirEntry { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -148,6 +180,8 @@ impl VFS for OriginPrivateFS { type DirEntry = DirEntry; + type File = web_sys::File; + fn root(&self) -> impl Future + 'static { std::future::ready(PathBuf::from(self.root.as_deref().unwrap().name())) } @@ -160,6 +194,10 @@ impl VFS for OriginPrivateFS { .unwrap_or(false) } + fn is_file_entry(&self, entry: &Self::DirEntry) -> bool { + entry.0.kind() == FileSystemHandleKind::File + } + async fn is_dir(&self, path: &Path) -> bool { let root = self.root.as_ref().expect("Uninitialized OPFS"); resolve_dir(root, path, false) @@ -168,47 +206,36 @@ impl VFS for OriginPrivateFS { .unwrap_or(false) } - async fn read_dir(&self, path: &Path) -> Result { - let root = self.root.as_ref().expect("Uninitialized OPFS"); - let dir = resolve_dir(root, path, false).await?; - let iter = - try_iter_async(dir.as_ref())?.ok_or_else(|| JsError::new("Not async iterable"))?; - Ok(DirIter(iter, path.to_owned())) + fn is_dir_entry(&self, entry: &Self::DirEntry) -> bool { + entry.0.kind() == FileSystemHandleKind::Directory } - fn dir_entry_path(&self, entry: &Self::DirEntry) -> PathBuf { - entry.1.clone() + async fn read_dir(&self, path: &Path) -> Result { + let dir = self.directory(path).await?; + dir.read_dir().await } - async fn read(&self, path: &Path) -> Result, Self::Error> { + async fn open(&self, path: &Path) -> Result { let root = self.root_dir()?; let file_handle = resolve_file(root, path).await?; - let file = JsFuture::from(file_handle.get_file()) - .await? - .unchecked_into::(); - let array_buffer = JsFuture::from(file.array_buffer()) - .await? - .unchecked_into::(); - let uint8_buf = js_sys::Uint8Array::new(&array_buffer); - Ok(uint8_buf.to_vec()) + let file = fs_file_handle_get_file(&file_handle).await?; + Ok(file) } -} - -pub fn try_iter_async(val: &JsValue) -> Result, JsValue> { - let async_iter_sym = Symbol::async_iterator(); - let iter_fn = Reflect::get(val, async_iter_sym.as_ref())?; - - let iter_fn: Function = match iter_fn.dyn_into() { - Ok(iter_fn) => iter_fn, - Err(_) => return Ok(None), - }; - let it: js_sys::AsyncIterator = match iter_fn.call0(val)?.dyn_into() { - Ok(it) => it, - Err(_) => return Ok(None), - }; + async fn read(&self, path: &Path) -> Result, Self::Error> { + let file = self.open(path).await?; + let uint8_buf = js_file_data(&file).await?; + Ok(uint8_buf.to_vec()) + } - Ok(Some(it)) + async fn open_dir_entry(&self, dir_entry: &Self::DirEntry) -> Result { + let file_handle = dir_entry + .0 + .dyn_ref::() + .ok_or_else(|| JsError::new("not a file"))?; + let file = fs_file_handle_get_file(file_handle).await?; + Ok(file) + } } impl OriginPrivateFS { @@ -223,6 +250,15 @@ impl OriginPrivateFS { } } + async fn directory(&self, path: &Path) -> Result { + let root = self.root.as_ref().expect("Uninitialized OPFS"); + let inner = resolve_dir(root, path, false).await?; + Ok(Directory { + inner, + path: path.to_owned(), + }) + } + pub async fn init(&mut self) -> Result<(), JsValue> { let dir_handle = FileSystemDirectoryHandle::unchecked_from_js( JsFuture::from(self.storage.get_directory()).await?, diff --git a/crates/signum/src/chsets/cache.rs b/crates/signum/src/chsets/cache.rs index eedd2d4..cce3572 100644 --- a/crates/signum/src/chsets/cache.rs +++ b/crates/signum/src/chsets/cache.rs @@ -1,8 +1,9 @@ //! # Implementation of a charset cache use std::{ + borrow::Cow, collections::HashMap, - fs::{self, ReadDir}, + fs, future::Future, io, path::{Path, PathBuf}, @@ -55,7 +56,7 @@ async fn find_font_file( while let Some(entry) = dir_iter.next().await { if let Ok(de) = entry { - let subfolder = fs.dir_entry_path(&de); + let subfolder = de.path(); if fs.is_dir(&subfolder).await { // Note: need to box the future here because this is async recursion. let fut = Box::pin(find_font_file(fs, &subfolder, name, extension)); @@ -198,6 +199,12 @@ pub trait AsyncIterator { fn next(&mut self) -> impl Future>; } +/// Directory entry for VFS +pub trait VfsDirEntry { + /// Return the path represented by this entry + fn path(&self) -> Cow<'_, Path>; +} + /// Virtual File System used for loading fonts pub trait VFS { /// Error type @@ -205,7 +212,9 @@ pub trait VFS { /// Directory iterator type DirIter: AsyncIterator>; /// Directory entry - type DirEntry; + type DirEntry: VfsDirEntry; + /// Open file + type File; /// Return the root path of the VFS fn root(&self) -> impl Future + 'static; @@ -213,17 +222,29 @@ pub trait VFS { /// Check whether the path is a file fn is_file(&self, path: &Path) -> impl Future; + /// Check whether the directory entry is a file + fn is_file_entry(&self, entry: &Self::DirEntry) -> bool; + /// Check whether the path is a directory fn is_dir(&self, path: &Path) -> impl Future; + /// Check whether the directory entry is a directory + fn is_dir_entry(&self, entry: &Self::DirEntry) -> bool; + /// Read a directory fn read_dir(&self, path: &Path) -> impl Future>; + /// Open a file + fn open(&self, path: &Path) -> impl Future>; + + /// Open a file + fn open_dir_entry( + &self, + dir_entry: &Self::DirEntry, + ) -> impl Future>; + /// Read a file fn read(&self, path: &Path) -> impl Future, Self::Error>>; - - /// Get the path of a directory entry - fn dir_entry_path(&self, entry: &Self::DirEntry) -> PathBuf; } /// VFS for the Local File System ([`std::fs`]) @@ -238,6 +259,12 @@ impl LocalFS { } } +impl VfsDirEntry for std::fs::DirEntry { + fn path(&self) -> Cow<'_, Path> { + Cow::Owned(std::fs::DirEntry::path(self)) + } +} + impl VFS for LocalFS { fn root(&self) -> impl Future + 'static { std::future::ready(self.chsets_folder.to_owned()) @@ -247,10 +274,18 @@ impl VFS for LocalFS { std::future::ready(path.is_file()) } + fn is_file_entry(&self, entry: &Self::DirEntry) -> bool { + entry.path().is_file() + } + fn is_dir(&self, path: &Path) -> impl Future { std::future::ready(path.is_dir()) } + fn is_dir_entry(&self, entry: &Self::DirEntry) -> bool { + entry.path().is_dir() + } + async fn read_dir(&self, path: &Path) -> Result { std::fs::read_dir(path) } @@ -261,16 +296,24 @@ impl VFS for LocalFS { type DirEntry = fs::DirEntry; - fn dir_entry_path(&self, entry: &Self::DirEntry) -> PathBuf { - entry.path() + type File = fs::File; + + fn open(&self, path: &Path) -> impl Future> { + std::future::ready(std::fs::File::open(path)) } fn read(&self, path: &Path) -> impl Future, Self::Error>> { - std::future::ready(std::fs::read(path)) + std::future::ready(std::fs::read(path)) // FIXME: async + } + + async fn open_dir_entry(&self, dir_entry: &Self::DirEntry) -> Result { + let path = dir_entry.path(); + let file = self.open(&path).await?; + Ok(file) } } -impl AsyncIterator for ReadDir { +impl AsyncIterator for fs::ReadDir { type Item = Result; async fn next(&mut self) -> Option {