Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

File System

The nightshade::filesystem module is a thin cross-platform I/O surface. On native it wraps the rfd crate for file dialogs and std::fs for reading and writing. On WebAssembly it routes through the browser, using Blob anchors for saves and <input type="file"> for loads. The function signatures are the same on both targets so calling code can avoid #[cfg] gates.

The trade-off is that the WASM side cannot expose a PathBuf. The browser sandbox does not give the page a filesystem path, only a name and a byte buffer. The cross-platform functions return LoadedFile (name plus bytes) and the path-based functions are native-only.

Feature requirements

FunctionNativeWASM
save_filefile_dialog featurealways available
request_file_loadfile_dialog featurealways available
pick_filefile_dialog featurenot available
pick_folderfile_dialog featurenot available
save_file_dialogfile_dialog featurenot available
read_filefile_dialog featurenot available
write_filefile_dialog featurenot available

The engine aggregate feature includes file_dialog by default.

Types

FileFilter

A filter entry describes a single named extension group. The dialog renders one entry per filter in its type dropdown. On WASM the filters become the accept attribute on the underlying <input type="file">.

#![allow(unused)]
fn main() {
use nightshade::filesystem::FileFilter;

let filters = [
    FileFilter {
        name: "JSON".to_string(),
        extensions: vec!["json".to_string()],
    },
    FileFilter {
        name: "Images".to_string(),
        extensions: vec!["png".to_string(), "jpg".to_string()],
    },
];
}

FileError

The error type returned by the path-based read and write functions:

  • FileError::NotFound(String) is a missing file at the given path.
  • FileError::ReadError(String) is a read that failed after open.
  • FileError::WriteError(String) is a write that failed after open.

FileError implements Display, so the error string formats cleanly for log output or UI.

LoadedFile

The shape of a file that has been read into memory. name is the original filename. bytes is the raw contents. The WASM target cannot supply a path, which is why the cross-platform load surface returns this struct rather than PathBuf plus Vec<u8>.

#![allow(unused)]
fn main() {
pub struct LoadedFile {
    pub name: String,
    pub bytes: Vec<u8>,
}
}

PendingFileLoad

A handle for an in-flight load. On native the file is read synchronously inside request_file_load and the handle is ready by the time you receive it. On WASM the browser hands the bytes back after the user chooses a file, which is one or more frames later, so the handle is initially empty and becomes ready when the read completes.

#![allow(unused)]
fn main() {
pub struct PendingFileLoad { /* ... */ }

impl PendingFileLoad {
    pub fn empty() -> Self;
    pub fn ready(file: LoadedFile) -> Self;
    pub fn is_ready(&self) -> bool;
    pub fn take(&self) -> Option<LoadedFile>;
}
}

take returns the file once and only once. Subsequent calls return None.

Cross-platform functions

These two functions compile on both native and WASM. Use them when a single code path is the goal.

save_file

save_file opens a save dialog on native and triggers a browser download on WASM. The first argument is the suggested filename, the second is the byte buffer to write, the third is the filter list:

#![allow(unused)]
fn main() {
use nightshade::filesystem::{save_file, FileFilter};

let filters = [FileFilter {
    name: "JSON".to_string(),
    extensions: vec!["json".to_string()],
}];

save_file("my_data.json", &bytes, &filters)?;
}

request_file_load

request_file_load opens a file picker and returns a PendingFileLoad. On native the read happens before the handle returns. On WASM the read happens after the user selects a file. The polling loop is the same in both cases:

#![allow(unused)]
fn main() {
use nightshade::filesystem::{request_file_load, FileFilter, PendingFileLoad};

let filters = [FileFilter {
    name: "JSON".to_string(),
    extensions: vec!["json".to_string()],
}];

let pending: PendingFileLoad = request_file_load(&filters);

if let Some(loaded) = pending.take() {
    println!("Loaded {} ({} bytes)", loaded.name, loaded.bytes.len());
}
}

Native-only functions

These are available only when targeting native with the file_dialog feature. They return PathBuf so workflows that track filesystem paths (loading assets by path, building a recent-files list, opening adjacent files) can use them directly.

pick_file

Opens a file picker and returns the chosen path, or None if the dialog was cancelled.

#![allow(unused)]
fn main() {
use nightshade::filesystem::{pick_file, FileFilter};

let filters = [FileFilter {
    name: "Scene files".to_string(),
    extensions: vec!["json".to_string(), "bin".to_string()],
}];

if let Some(path) = pick_file(&filters) {
    // use path
}
}

pick_folder

Opens a folder picker. Returns the chosen folder path, or None if cancelled.

#![allow(unused)]
fn main() {
use nightshade::filesystem::pick_folder;

if let Some(folder) = pick_folder() {
    // use folder path
}
}

save_file_dialog

Opens a save dialog and returns the path the user picked, without writing anything to it. The caller writes the bytes themselves. The second argument is an optional default filename suggestion.

#![allow(unused)]
fn main() {
use nightshade::filesystem::{save_file_dialog, FileFilter};

let filters = [FileFilter {
    name: "Project".to_string(),
    extensions: vec!["project.json".to_string()],
}];

if let Some(path) = save_file_dialog(&filters, Some("my_project.json")) {
    // write to path yourself
}
}

read_file

Reads a file at the given path into a byte vector. Returns FileError::NotFound if the file does not exist, FileError::ReadError if the read fails after open.

#![allow(unused)]
fn main() {
use nightshade::filesystem::read_file;

let bytes = read_file(std::path::Path::new("settings.json"))?;
let settings: MySettings = serde_json::from_slice(&bytes)?;
}

write_file

Writes bytes to a file at the given path. Parent directories are created if they do not exist, so callers do not have to mkdir before writing into a fresh subtree.

#![allow(unused)]
fn main() {
use nightshade::filesystem::write_file;

let json = serde_json::to_vec_pretty(&settings)?;
write_file(std::path::Path::new("settings.json"), &json)?;
}

Polling pattern

The cross-platform load is asynchronous on WASM, so the natural shape is to store the PendingFileLoad in your state and poll it each frame. This pattern is identical on native and WASM and needs no #[cfg] gates:

#![allow(unused)]
fn main() {
struct MyApp {
    pending_load: Option<nightshade::filesystem::PendingFileLoad>,
}

self.pending_load = Some(nightshade::filesystem::request_file_load(&filters));

if let Some(ref pending) = self.pending_load {
    if let Some(file) = pending.take() {
        self.pending_load = None;
        self.process_loaded_file(&file.name, &file.bytes);
    }
}
}

The native path resolves on the first poll. The WASM path resolves whenever the browser finishes reading the file. Caller code is the same in both cases.