The user's files through ltk::files, alike natively and in a browser, and a shell's requests answered in a browser through ltk::web::shell_request
An app read the user's files with `std::fs`, which a browser does not have: there a page reaches a folder of the user's computer only through forge/web, which holds it through the File System Access API and answers asynchronously (forge/web/DESIGN.md, section 11). An app that builds for both, such as Crate, had no way to list, read or write a file in a browser, and a page's memory cannot hold a large file whole. ltk::files. Asynchronous operations on the user's files whose results reach the app as messages through its `ChannelSender`: `list`, `stat`, `read`, `read_range`, `write`, `open_write`, `create_dir`, `rename`, `remove`, `remove_all`, `copy`, `copy_all`, `mount` and `unmount`, each taking the sender and a `done` that turns the `io::Result` into a message. Results come in no set order, so an app that has moved on checks that one is still the one it waits for. Errors are the same `io::ErrorKind`s on every platform, `Interrupted` for a cancelled copy. `Entry` and `Metadata` give a file's kind, size and times, the access time natively only. `ltk::vfs` stays what reads an app's own resources. Natively. `std::fs` on one worker thread of ltk's, in the order the app asked (`files/native.rs`), and each copy on a thread of its own, so a long one holds nothing else up (`files/copy.rs`). A file written whole or through a `Writer` goes beside its target as a temporary file, synced and renamed over it on commit and removed if dropped before, so a file being written is never seen half done. `rename` never replaces what is at `to`, as in a browser. In a browser. Each operation is one of forge/web's `fs_*` requests over the app's connection (`platform/web/files.rs`), carrying a number its `fs_reply` answers with; contents travel as a `Uint8Array` beside the JSON, the second argument of the connection's `request` and of the client's event handler, so `on_event` now takes it. What is asked before the app has connected is sent once it has; on a page without a desktop, or once the connection has closed, it fails with `Unsupported`. forge/web's error codes become their `io::ErrorKind`s, and `fs_progress` reaches a copy's `progress`. Without the web backend, on wasm32 every operation fails with `Unsupported` (`files/unsupported.rs`). Chunks. Contents never travel whole unless they are small: a large file is read with `read_range` and written through a `Writer`, chunk by chunk, `files::CHUNK` (1 MiB) at a time, the next sent once the last is answered. A `Writer` dropped without `close` is aborted and the file left as it was; in a browser a new file shows empty until closed. A copy runs where the files are, its contents never passing through the app: it reports its progress every 100 ms, never replaces an existing file, and, cancelled through its `Operation` or failed, leaves nothing behind. Mounts and the desktop's notices. `mount` asks the browser for a folder of the user's computer, from the user's press, and `unmount` lets it go; natively both fail with `Unsupported`. `App::on_files_changed`, `App::on_mounts_changed` and `App::on_open_files` hear of forge/web's `fs_changed`, `fs_mounts` and `open_files`, which the web backend queues as page events; natively they are not called yet. Types. `files::mime_type( name )` gives a file's type by shared-mime-info's `globs2`, with its weights, its case-sensitive globs and fnmatch's `*`, `?` and bracket classes, from the resource directory's `mime/` first, then the user's and the system's. `scripts/android-assets.sh`, and so `web-assets.sh`, now copies shared-mime-info's `globs2` and `subclasses` into the tree's `mime/` (`MIME_DIR` overrides where from), so a page has them. ltk::web::shell_request. A shell message that waits for forge/web's reply, such as `get_window_thumbnail`: sent as `shell_send` sends one and answered at once with forge/web's reply as JSON, or `None` before the app has connected, since a request is not kept for later, and when forge/web replies nothing. A shell's requests that wait for an answer natively got none in a browser. `shell.rs` parses a message once for both, and `send` returns forge/web's reply. Tests cover writing and committing, aborting, listing, ranges, removing, copying a tree with its progress, a cancelled copy, a copy that would replace, and the globs. `CHANGELOG.md` describes the changes.
This commit is contained in:
@@ -12,7 +12,7 @@ use crate::app::{ App, Layer, ShellMode, ToplevelEvent };
|
||||
use crate::event_loop::AppData;
|
||||
|
||||
use super::run::{ push, PageEvent };
|
||||
use super::shell;
|
||||
use super::{ files, shell };
|
||||
|
||||
/// The id the main surface goes by.
|
||||
pub( super ) const MAIN: u32 = 0;
|
||||
@@ -21,7 +21,7 @@ pub( crate ) struct Connection
|
||||
{
|
||||
request: js_sys::Function,
|
||||
disconnect: js_sys::Function,
|
||||
_on_event: Closure<dyn FnMut( String )>,
|
||||
_on_event: Closure<dyn FnMut( String, JsValue )>,
|
||||
}
|
||||
|
||||
impl Connection
|
||||
@@ -31,11 +31,12 @@ impl Connection
|
||||
{
|
||||
let api = js_sys::Reflect::get( &js_sys::global(), &"forgeWeb".into() ).ok().filter( |v| v.is_object() )?;
|
||||
let connect: js_sys::Function = js_sys::Reflect::get( &api, &"connect".into() ).ok()?.dyn_into().ok()?;
|
||||
let on_event = Closure::<dyn FnMut( String )>::new( on_event );
|
||||
let on_event = Closure::<dyn FnMut( String, JsValue )>::new( on_event );
|
||||
let connection = connect.call2( &api, &JsValue::from_str( app_id ), on_event.as_ref() ).ok()?;
|
||||
let method = |name: &str| js_sys::Reflect::get( &connection, &name.into() ).ok().and_then( |f| f.dyn_into::<js_sys::Function>().ok() );
|
||||
let this = Self { request: method( "request" )?, disconnect: method( "disconnect" )?, _on_event: on_event };
|
||||
shell::connected( Some( this.request.clone() ) );
|
||||
files::connected( Some( this.request.clone() ) );
|
||||
if let Err( e ) = this.request( &json!( { "type": "list_toplevels" } ) )
|
||||
{
|
||||
web_sys::console::warn_1( &format!( "[ltk] list_toplevels: {e}" ).into() );
|
||||
@@ -71,11 +72,13 @@ impl Connection
|
||||
pub( super ) fn close( &self )
|
||||
{
|
||||
shell::connected( None );
|
||||
files::connected( None );
|
||||
let _ = self.disconnect.call0( &JsValue::NULL );
|
||||
}
|
||||
}
|
||||
|
||||
fn on_event( json: String )
|
||||
/// One of forge/web's events, with the file contents `data` carries for an `fs_reply`.
|
||||
fn on_event( json: String, data: JsValue )
|
||||
{
|
||||
let Ok( event ) = serde_json::from_str::<Value>( &json ) else { return };
|
||||
let surface = event[ "surface" ].as_u64().unwrap_or( 0 ) as u32;
|
||||
@@ -98,6 +101,11 @@ fn on_event( json: String )
|
||||
title: event[ "title" ].as_str().unwrap_or_default().to_string(),
|
||||
} ) ),
|
||||
Some( "toplevel_closed" ) => push( PageEvent::Toplevel( ToplevelEvent::Closed { id: event[ "id" ].as_u64().unwrap_or( 0 ) as u32 } ) ),
|
||||
Some( "fs_reply" ) => files::reply( &event, data ),
|
||||
Some( "fs_progress" ) => files::progress( &event ),
|
||||
Some( "fs_changed" ) => push( PageEvent::FilesChanged( event[ "path" ].as_str().unwrap_or_default().into() ) ),
|
||||
Some( "fs_mounts" ) => push( PageEvent::Mounts( files::mounts( &event ) ) ),
|
||||
Some( "open_files" ) => push( PageEvent::OpenFiles( files::paths( &event ) ) ),
|
||||
_ => web_sys::console::warn_1( &format!( "[ltk] forge/web: {json}" ).into() ),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user