use crate::filesystem::EntryKind;
use super::dom;
use super::templates;
use super::APP;
pub(crate) async fn refresh() {
let cwd = APP.with(|cell| cell.borrow().opfs_cwd.clone());
let fs = super::shared_opfs();
let path = cwd_path(&cwd);
dom::swap_inner(
"fs-breadcrumb",
&templates::opfs_breadcrumb(&cwd).into_string(),
);
match fs.read_dir(&path).await {
Ok(mut entries) => {
entries.sort_by(|a, b| {
let a_dir = matches!(a.kind, EntryKind::Directory);
let b_dir = matches!(b.kind, EntryKind::Directory);
match (a_dir, b_dir) {
(true, false) => std::cmp::Ordering::Less,
(false, true) => std::cmp::Ordering::Greater,
_ => a.name.cmp(&b.name),
}
});
dom::swap_inner(
"fs-list",
&templates::opfs_list(&cwd, &entries).into_string(),
);
}
Err(err) => {
dom::swap_inner(
"fs-list",
&templates::opfs_error(&format!("{err}")).into_string(),
);
}
}
}
pub(crate) async fn navigate(target: &str) {
ensure_modal_open().await;
let new_cwd: Vec<String> = if target.is_empty() {
Vec::new()
} else {
target
.split('/')
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
.collect()
};
APP.with(|cell| cell.borrow_mut().opfs_cwd = new_cwd);
close_viewer();
refresh().await;
}
pub(crate) async fn open_file(name: &str) {
let lower = name.to_ascii_lowercase();
if lower.ends_with(".wasm") {
display_file(name).await
} else if lower.ends_with(".rl") {
run_cartridge_file(name).await
} else if lower.ends_with(".html") || lower.ends_with(".htm") {
render_html_file(name).await
} else {
edit_file(name).await
}
}
pub(crate) async fn display_file(name: &str) {
let (path, display_path) = resolve_path(name);
let fs = super::shared_opfs();
match fs.read(&path).await {
Ok(bytes) => {
if let Err(err) = super::display::run_wasm(&bytes).await {
super::dom::set_status(&format!("display {display_path}: {err:?}"), true);
}
}
Err(err) => {
super::dom::set_status(&format!("display {display_path}: {err}"), true);
}
}
}
pub(crate) async fn run_cartridge_file(name: &str) {
let (path, display_path) = resolve_path(name);
let fs = super::shared_opfs();
let bytes = match fs.read(&path).await {
Ok(b) => b,
Err(err) => {
super::dom::set_status(&format!("display {display_path}: {err}"), true);
return;
}
};
let source = String::from_utf8_lossy(&bytes);
match crate::rustlite::compile(&source) {
Ok(wasm) => {
if let Err(err) = super::display::run_wasm(&wasm).await {
super::dom::set_status(&format!("display {display_path}: {err:?}"), true);
}
}
Err(err) => {
super::dom::set_status(&format!("compile {display_path}: {err}"), true);
}
}
}
pub(crate) async fn render_html_file(name: &str) {
let (path, display_path) = resolve_path(name);
let fs = super::shared_opfs();
match fs.read(&path).await {
Ok(bytes) => {
let source = String::from_utf8_lossy(&bytes);
if let Err(err) = super::display::render_html(&source) {
super::dom::set_status(&format!("display {display_path}: {err:?}"), true);
}
}
Err(err) => {
super::dom::set_status(&format!("display {display_path}: {err}"), true);
}
}
}
pub(crate) async fn edit_file(name: &str) {
ensure_modal_open().await;
let (path, display_path) = resolve_path(name);
let fs = super::shared_opfs();
const MAX_EDIT: usize = 1024 * 1024;
match fs.read(&path).await {
Ok(bytes) if bytes.len() > MAX_EDIT => {
super::dom::set_status(
&format!(
"{display_path}: too large to edit in-tab ({} bytes > {MAX_EDIT})",
bytes.len()
),
true,
);
}
Ok(bytes) => {
let text = String::from_utf8_lossy(&bytes).into_owned();
dom::swap_inner(
"fs-viewer",
&templates::opfs_editor(&display_path, name, &text).into_string(),
);
if let Some(ta) = dom::textarea_by_id("fs-editor") {
let _ = ta.focus();
}
}
Err(err) => {
super::dom::set_status(&format!("edit {display_path}: {err}"), true);
}
}
}
pub(crate) async fn save_file(name: &str) {
let Some(editor) = dom::textarea_by_id("fs-editor") else {
super::dom::set_status("save: editor textarea missing", true);
return;
};
let contents = editor.value();
let (path, display_path) = resolve_path(name);
let fs = super::shared_opfs();
if let Err(err) = fs.write_atomic(&path, contents.as_bytes()).await {
super::dom::set_status(&format!("save {display_path}: {err}"), true);
return;
}
super::dom::set_status(&format!("saved {display_path} ({} bytes)", contents.len()), false);
open_file(name).await;
refresh().await;
}
fn resolve_path(name: &str) -> (String, String) {
let cwd = APP.with(|cell| cell.borrow().opfs_cwd.clone());
let mut path = cwd_path(&cwd);
if !path.ends_with('/') {
path.push('/');
}
path.push_str(name);
let display = if cwd.is_empty() {
format!("/{name}")
} else {
format!("/{}/{name}", cwd.join("/"))
};
(path, display)
}
pub(crate) async fn wipe() {
let fs = super::shared_opfs();
let entries = match fs.read_dir("").await {
Ok(es) => es,
Err(err) => {
super::dom::set_status(&format!("wipe: {err}"), true);
return;
}
};
let mut failed: Vec<String> = Vec::new();
for entry in entries {
if let Err(err) = fs.delete(&entry.name).await {
failed.push(format!("{}: {err}", entry.name));
}
}
APP.with(|cell| cell.borrow_mut().opfs_cwd.clear());
close_viewer();
refresh().await;
if failed.is_empty() {
super::dom::set_status("OPFS wiped.", false);
} else {
super::dom::set_status(
&format!("OPFS partial wipe — {} entries failed", failed.len()),
true,
);
}
}
pub(crate) fn close_viewer() {
dom::swap_inner("fs-viewer", "");
}
fn files_modal_open() -> bool {
dom::by_id("fs-list").is_some()
}
async fn ensure_modal_open() {
if files_modal_open() {
return;
}
dom::swap_outer("files-modal", &templates::files_modal().into_string());
refresh().await;
}
pub(crate) async fn toggle_files_modal() {
if files_modal_open() {
dom::swap_outer("files-modal", &templates::files_modal_closed().into_string());
dom::restore_focus();
} else {
dom::remember_focus();
ensure_modal_open().await;
dom::focus_first_in("files-modal");
}
}
pub(crate) fn toggle_display() {
if dom::by_id("display-canvas").is_some() {
close_display();
} else {
dom::remember_focus();
dom::swap_outer("display-overlay", &templates::display_overlay().into_string());
dom::focus_first_in("display-overlay");
}
}
pub(crate) fn close_display() {
super::display::stop();
dom::swap_outer("display-overlay", &templates::display_overlay_closed().into_string());
dom::restore_focus();
}
fn cwd_path(cwd: &[String]) -> String {
if cwd.is_empty() {
"".to_string()
} else {
cwd.join("/")
}
}