src/webio.rs
//! Browser-side I/O helpers (wasm-only): file reads and named downloads.
use wasm_bindgen::JsCast;
/// Hand the bytes to the browser as a named download via a temporary object
/// URL. The URL is revoked immediately after the click — the blob lives on
/// until the download completes.
pub fn save_bytes(file_name: &str, data: &[u8], content_type: &str) -> Result<(), String> {
let array = js_sys::Uint8Array::from(data);
let parts = js_sys::Array::of1(&array.buffer());
let opts = web_sys::BlobPropertyBag::new();
if !content_type.is_empty() {
opts.set_type(content_type);
}
let blob = web_sys::Blob::new_with_buffer_source_sequence_and_options(&parts, &opts)
.map_err(|_| "blob construction failed")?;
let url = web_sys::Url::create_object_url_with_blob(&blob)
.map_err(|_| "object URL creation failed")?;
let document = web_sys::window()
.and_then(|w| w.document())
.ok_or("no document")?;
let a: web_sys::HtmlAnchorElement = document
.create_element("a")
.map_err(|_| "createElement failed")?
.dyn_into()
.map_err(|_| "anchor cast failed")?;
a.set_href(&url);
a.set_download(file_name);
a.click();
let _ = web_sys::Url::revoke_object_url(&url);
Ok(())
}
/// Read the picked file's bytes off the main thread via `File.arrayBuffer()`.
pub async fn read_file(file: &web_sys::File) -> Result<Vec<u8>, String> {
let buf = wasm_bindgen_futures::JsFuture::from(file.array_buffer())
.await
.map_err(|_| "arrayBuffer() rejected")?;
Ok(js_sys::Uint8Array::new(&buf).to_vec())
}
/// Set the browser-tab title — every routed view names itself.
pub fn set_title(title: &str) {
if let Some(doc) = web_sys::window().and_then(|w| w.document()) {
doc.set_title(&format!("{title} · holger console"));
}
}
/// Local wall-clock HH:MM:SS — mutation results carry WHEN they happened.
pub fn now_hms() -> String {
let d = js_sys::Date::new_0();
format!(
"{:02}:{:02}:{:02}",
d.get_hours(),
d.get_minutes(),
d.get_seconds()
)
}
/// Put text on the clipboard (async Clipboard API; falls back to a rejected
/// future outside secure contexts — the caller reports, never panics).
pub async fn copy_text(text: &str) -> Result<(), String> {
let clipboard = web_sys::window()
.map(|w| w.navigator().clipboard())
.ok_or("no window")?;
wasm_bindgen_futures::JsFuture::from(clipboard.write_text(text))
.await
.map(|_| ())
.map_err(|_| "clipboard write refused (needs a secure context)".to_string())
}