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() ),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-only
|
||||
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
||||
|
||||
//! `ltk::files` in a browser: forge/web's file requests over the app's connection (forge/web/DESIGN.md, section 11). Each request carries a number its `fs_reply` answers with; contents travel as a `Uint8Array` beside the JSON, as the request's and the event's second argument. What is asked before the app has connected waits for the connection, and fails with `Unsupported` on a page without a desktop.
|
||||
|
||||
use std::cell::{ Cell, RefCell };
|
||||
use std::collections::HashMap;
|
||||
use std::io;
|
||||
use std::path::{ Path, PathBuf };
|
||||
use std::time::{ Duration, UNIX_EPOCH };
|
||||
|
||||
use serde_json::{ json, Value };
|
||||
use wasm_bindgen::{ JsCast, JsValue };
|
||||
|
||||
use crate::files::{ Entry, Kind, Metadata, Mount, Progress, Reply };
|
||||
|
||||
type Answer = io::Result<( Value, Option<Vec<u8>> )>;
|
||||
type OnReply = Box<dyn FnOnce( Answer )>;
|
||||
|
||||
enum Link
|
||||
{
|
||||
Waiting( Vec<( Value, Option<Vec<u8>> )> ),
|
||||
Open( js_sys::Function ),
|
||||
Absent,
|
||||
}
|
||||
|
||||
thread_local!
|
||||
{
|
||||
static LINK: RefCell<Link> = const { RefCell::new( Link::Waiting( Vec::new() ) ) };
|
||||
static NEXT: Cell<u64> = const { Cell::new( 1 ) };
|
||||
static REPLIES: RefCell<HashMap<u64, OnReply>> = RefCell::new( HashMap::new() );
|
||||
static PROGRESS: RefCell<HashMap<u64, Progress>> = RefCell::new( HashMap::new() );
|
||||
}
|
||||
|
||||
pub type WriterHandle = u64;
|
||||
pub type OperationHandle = u64;
|
||||
|
||||
pub fn list( path: PathBuf, reply: Reply<Vec<Entry>> )
|
||||
{
|
||||
request( json!( { "type": "fs_list", "path": name( &path ) } ), None, Box::new( move |answer| reply( answer.map( |( value, _ )| entries( &value ) ) ) ) );
|
||||
}
|
||||
|
||||
pub fn stat( path: PathBuf, reply: Reply<Metadata> )
|
||||
{
|
||||
request( json!( { "type": "fs_stat", "path": name( &path ) } ), None, Box::new( move |answer|
|
||||
{
|
||||
reply( answer.and_then( |( value, _ )| metadata( &value ).ok_or_else( || io::ErrorKind::InvalidData.into() ) ) )
|
||||
} ) );
|
||||
}
|
||||
|
||||
pub fn read( path: PathBuf, range: Option<( u64, u64 )>, reply: Reply<Vec<u8>> )
|
||||
{
|
||||
let mut message = json!( { "type": "fs_read", "path": name( &path ) } );
|
||||
if let Some( ( offset, length ) ) = range
|
||||
{
|
||||
message[ "offset" ] = offset.into();
|
||||
message[ "length" ] = length.into();
|
||||
}
|
||||
request( message, None, Box::new( move |answer| reply( answer.map( |( _, data )| data.unwrap_or_default() ) ) ) );
|
||||
}
|
||||
|
||||
pub fn write( path: PathBuf, data: Vec<u8>, reply: Reply<()> )
|
||||
{
|
||||
request( json!( { "type": "fs_write", "path": name( &path ) } ), Some( data ), done( reply ) );
|
||||
}
|
||||
|
||||
pub fn open_write( path: PathBuf, reply: Reply<WriterHandle> )
|
||||
{
|
||||
request( json!( { "type": "fs_open_write", "path": name( &path ) } ), None, Box::new( move |answer|
|
||||
{
|
||||
reply( answer.and_then( |( value, _ )| value[ "handle" ].as_u64().ok_or_else( || io::ErrorKind::InvalidData.into() ) ) )
|
||||
} ) );
|
||||
}
|
||||
|
||||
pub fn write_chunk( handle: &WriterHandle, chunk: Vec<u8>, reply: Reply<()> )
|
||||
{
|
||||
request( json!( { "type": "fs_write_chunk", "handle": handle } ), Some( chunk ), done( reply ) );
|
||||
}
|
||||
|
||||
pub fn close( handle: WriterHandle, reply: Reply<()> )
|
||||
{
|
||||
request( json!( { "type": "fs_close", "handle": handle } ), None, done( reply ) );
|
||||
}
|
||||
|
||||
pub fn abort( handle: WriterHandle )
|
||||
{
|
||||
request( json!( { "type": "fs_abort", "handle": handle } ), None, Box::new( |_| {} ) );
|
||||
}
|
||||
|
||||
pub fn create_dir( path: PathBuf, reply: Reply<()> )
|
||||
{
|
||||
request( json!( { "type": "fs_mkdir", "path": name( &path ) } ), None, done( reply ) );
|
||||
}
|
||||
|
||||
pub fn rename( from: PathBuf, to: PathBuf, reply: Reply<()> )
|
||||
{
|
||||
request( json!( { "type": "fs_rename", "from": name( &from ), "to": name( &to ) } ), None, done( reply ) );
|
||||
}
|
||||
|
||||
pub fn remove( path: PathBuf, all: bool, reply: Reply<()> )
|
||||
{
|
||||
request( json!( { "type": "fs_remove", "path": name( &path ), "recursive": all } ), None, done( reply ) );
|
||||
}
|
||||
|
||||
pub fn copy( from: PathBuf, to: PathBuf, all: bool, progress: Progress, reply: Reply<()> ) -> OperationHandle
|
||||
{
|
||||
let kind = if all { "fs_copy_all" } else { "fs_copy" };
|
||||
let id = next_id();
|
||||
PROGRESS.with( |p| p.borrow_mut().insert( id, progress ) );
|
||||
send( id, json!( { "type": kind, "from": name( &from ), "to": name( &to ) } ), None, done( reply ) );
|
||||
id
|
||||
}
|
||||
|
||||
pub fn cancel( handle: &OperationHandle )
|
||||
{
|
||||
request( json!( { "type": "fs_cancel", "copy": handle } ), None, Box::new( |_| {} ) );
|
||||
}
|
||||
|
||||
pub fn mount( reply: Reply<Mount> )
|
||||
{
|
||||
request( json!( { "type": "fs_mount" } ), None, Box::new( move |answer|
|
||||
{
|
||||
reply( answer.and_then( |( value, _ )| mount_of( &value ).ok_or_else( || io::ErrorKind::InvalidData.into() ) ) )
|
||||
} ) );
|
||||
}
|
||||
|
||||
pub fn unmount( path: PathBuf, reply: Reply<()> )
|
||||
{
|
||||
request( json!( { "type": "fs_unmount", "path": name( &path ) } ), None, done( reply ) );
|
||||
}
|
||||
|
||||
/// The connection's `request` once connected; `None` when there is no desktop or it has closed, which fails what is waiting.
|
||||
pub( super ) fn connected( request: Option<js_sys::Function> )
|
||||
{
|
||||
let waiting = LINK.with( |l|
|
||||
{
|
||||
let link = match &request { Some( f ) => Link::Open( f.clone() ), None => Link::Absent };
|
||||
match std::mem::replace( &mut *l.borrow_mut(), link )
|
||||
{
|
||||
Link::Waiting( queue ) => queue,
|
||||
_ => Vec::new(),
|
||||
}
|
||||
} );
|
||||
match request
|
||||
{
|
||||
Some( request ) =>
|
||||
{
|
||||
for ( message, data ) in waiting
|
||||
{
|
||||
let id = message[ "request" ].as_u64().unwrap_or_default();
|
||||
transmit( &request, id, &message, data );
|
||||
}
|
||||
}
|
||||
None =>
|
||||
{
|
||||
let pending: Vec<u64> = REPLIES.with( |r| r.borrow().keys().copied().collect() );
|
||||
for id in pending { answer( id, Err( io::ErrorKind::Unsupported.into() ) ); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// forge/web's `fs_reply`, with the contents it carries as `data`.
|
||||
pub( super ) fn reply( event: &Value, data: JsValue )
|
||||
{
|
||||
let Some( id ) = event[ "request" ].as_u64() else { return };
|
||||
let result = match event[ "error" ].as_str()
|
||||
{
|
||||
Some( code ) => Err( error( code ) ),
|
||||
None => Ok( ( event.clone(), data.dyn_ref::<js_sys::Uint8Array>().map( js_sys::Uint8Array::to_vec ) ) ),
|
||||
};
|
||||
answer( id, result );
|
||||
}
|
||||
|
||||
/// forge/web's `fs_progress` for a copy.
|
||||
pub( super ) fn progress( event: &Value )
|
||||
{
|
||||
let Some( id ) = event[ "request" ].as_u64() else { return };
|
||||
let copied = event[ "done" ].as_u64().unwrap_or_default();
|
||||
let total = event[ "total" ].as_u64().unwrap_or_default();
|
||||
PROGRESS.with( |p| { if let Some( progress ) = p.borrow().get( &id ) { progress( copied, total ); } } );
|
||||
}
|
||||
|
||||
pub( super ) fn mounts( event: &Value ) -> Vec<Mount>
|
||||
{
|
||||
event[ "mounts" ].as_array().map( |list| list.iter().filter_map( mount_of ).collect() ).unwrap_or_default()
|
||||
}
|
||||
|
||||
pub( super ) fn paths( event: &Value ) -> Vec<PathBuf>
|
||||
{
|
||||
event[ "paths" ].as_array().map( |list| list.iter().filter_map( Value::as_str ).map( PathBuf::from ).collect() ).unwrap_or_default()
|
||||
}
|
||||
|
||||
fn next_id() -> u64
|
||||
{
|
||||
NEXT.with( |n| { let id = n.get(); n.set( id + 1 ); id } )
|
||||
}
|
||||
|
||||
fn request( message: Value, data: Option<Vec<u8>>, on_reply: OnReply )
|
||||
{
|
||||
send( next_id(), message, data, on_reply );
|
||||
}
|
||||
|
||||
fn send( id: u64, mut message: Value, data: Option<Vec<u8>>, on_reply: OnReply )
|
||||
{
|
||||
message[ "request" ] = id.into();
|
||||
REPLIES.with( |r| r.borrow_mut().insert( id, on_reply ) );
|
||||
let unsent = LINK.with( move |l|
|
||||
{
|
||||
match &mut *l.borrow_mut()
|
||||
{
|
||||
Link::Waiting( queue ) => { queue.push( ( message, data ) ); None }
|
||||
Link::Open( request ) => Some( ( Some( request.clone() ), message, data ) ),
|
||||
Link::Absent => Some( ( None, message, data ) ),
|
||||
}
|
||||
} );
|
||||
match unsent
|
||||
{
|
||||
Some( ( Some( request ), message, data ) ) => transmit( &request, id, &message, data ),
|
||||
Some( ( None, _, _ ) ) => answer( id, Err( io::ErrorKind::Unsupported.into() ) ),
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn transmit( request: &js_sys::Function, id: u64, message: &Value, data: Option<Vec<u8>> )
|
||||
{
|
||||
let data = data.map_or( JsValue::UNDEFINED, |data| js_sys::Uint8Array::from( data.as_slice() ).into() );
|
||||
if let Err( e ) = request.call2( &JsValue::NULL, &JsValue::from_str( &message.to_string() ), &data )
|
||||
{
|
||||
let reason = e.as_string().unwrap_or_else( || format!( "{e:?}" ) );
|
||||
answer( id, Err( io::Error::other( reason ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
fn answer( id: u64, result: Answer )
|
||||
{
|
||||
PROGRESS.with( |p| p.borrow_mut().remove( &id ) );
|
||||
let Some( on_reply ) = REPLIES.with( |r| r.borrow_mut().remove( &id ) ) else { return };
|
||||
on_reply( result );
|
||||
}
|
||||
|
||||
fn done( reply: Reply<()> ) -> OnReply
|
||||
{
|
||||
Box::new( move |answer| reply( answer.map( |_| () ) ) )
|
||||
}
|
||||
|
||||
fn name( path: &Path ) -> String
|
||||
{
|
||||
path.to_string_lossy().into_owned()
|
||||
}
|
||||
|
||||
fn error( code: &str ) -> io::Error
|
||||
{
|
||||
let kind = match code
|
||||
{
|
||||
"not_found" => io::ErrorKind::NotFound,
|
||||
"exists" => io::ErrorKind::AlreadyExists,
|
||||
"not_a_directory" => io::ErrorKind::NotADirectory,
|
||||
"is_a_directory" => io::ErrorKind::IsADirectory,
|
||||
"not_empty" => io::ErrorKind::DirectoryNotEmpty,
|
||||
"denied" => io::ErrorKind::PermissionDenied,
|
||||
"unsupported" => io::ErrorKind::Unsupported,
|
||||
"cancelled" => io::ErrorKind::Interrupted,
|
||||
_ => io::ErrorKind::Other,
|
||||
};
|
||||
io::Error::new( kind, code.to_string() )
|
||||
}
|
||||
|
||||
fn metadata( value: &Value ) -> Option<Metadata>
|
||||
{
|
||||
let kind = match value[ "kind" ].as_str()? { "directory" => Kind::Directory, _ => Kind::File };
|
||||
Some( Metadata
|
||||
{
|
||||
kind,
|
||||
size: value[ "size" ].as_u64().unwrap_or_default(),
|
||||
modified: value[ "modified" ].as_f64().map( |ms| UNIX_EPOCH + Duration::from_millis( ms.max( 0.0 ) as u64 ) ),
|
||||
accessed: None,
|
||||
} )
|
||||
}
|
||||
|
||||
fn entries( value: &Value ) -> Vec<Entry>
|
||||
{
|
||||
let Some( list ) = value[ "entries" ].as_array() else { return Vec::new() };
|
||||
list.iter().filter_map( |entry|
|
||||
{
|
||||
let meta = metadata( entry )?;
|
||||
Some( Entry { name: entry[ "name" ].as_str()?.into(), kind: meta.kind, size: meta.size, modified: meta.modified, accessed: None } )
|
||||
} ).collect()
|
||||
}
|
||||
|
||||
fn mount_of( value: &Value ) -> Option<Mount>
|
||||
{
|
||||
Some( Mount { path: value[ "path" ].as_str()?.into(), name: value[ "name" ].as_str()?.to_string() } )
|
||||
}
|
||||
@@ -7,7 +7,8 @@
|
||||
//! | --- | --- |
|
||||
//! | `assets.rs` | The themes, fonts and keyboard layouts a page serves, fetched into memory as the app's resource directory |
|
||||
//! | `desktop.rs` | The connection to forge/web and the main surface's role |
|
||||
//! | `shell.rs` | The shell protocol over that connection, for a shell such as crustace: [`shell_send`] and [`set_shell_listener`] |
|
||||
//! | `shell.rs` | The shell protocol over that connection, for a shell such as crustace: [`shell_send`], [`shell_request`] and [`set_shell_listener`] |
|
||||
//! | `files.rs` | [`crate::files`] over that connection: forge/web's file requests, their replies and a copy's progress |
|
||||
//! | `overlays.rs` | The app's overlays as surfaces of the desktop, kept in step with `App::overlays()` |
|
||||
//! | `subsurfaces.rs` | The app's subsurfaces as surfaces of the desktop, painted when their content changes and otherwise only moved |
|
||||
//! | `log.rs` | Panics into the browser's console |
|
||||
@@ -25,6 +26,7 @@ mod assets;
|
||||
mod clipboard;
|
||||
mod cursor;
|
||||
mod desktop;
|
||||
pub( crate ) mod files;
|
||||
mod input;
|
||||
mod keys;
|
||||
mod log;
|
||||
@@ -38,7 +40,7 @@ mod surface;
|
||||
|
||||
pub use assets::{ load_resources, load_tree };
|
||||
pub use run::set_canvas;
|
||||
pub use shell::{ set_shell_listener, shell_send };
|
||||
pub use shell::{ set_shell_listener, shell_request, shell_send };
|
||||
pub( crate ) use state::WebPlatform;
|
||||
pub( crate ) use surface::WebSurfaceState;
|
||||
|
||||
|
||||
@@ -44,6 +44,11 @@ pub( super ) enum PageEvent
|
||||
},
|
||||
/// A window of the desktop's, as `ext-foreign-toplevel-list` reports it.
|
||||
Toplevel( ToplevelEvent ),
|
||||
/// A directory another client changed.
|
||||
FilesChanged( std::path::PathBuf ),
|
||||
Mounts( Vec<crate::files::Mount> ),
|
||||
/// The files the app was launched to open.
|
||||
OpenFiles( Vec<std::path::PathBuf> ),
|
||||
}
|
||||
|
||||
/// An event listener, registered for as long as it lives.
|
||||
@@ -126,6 +131,7 @@ pub( crate ) fn try_run<A: App>( app: A ) -> Result<(), RunError>
|
||||
// A canvas the page named wins; else the page's desktop gives the main surface, and without one ltk lays a canvas over the page.
|
||||
let given = CANVAS.with( |c| c.borrow_mut().take() );
|
||||
let desktop = if given.is_none() { Connection::open( app.app_id() ) } else { None };
|
||||
if desktop.is_none() { super::files::connected( None ); }
|
||||
let ( canvas, target, own ) = match ( desktop.as_ref(), given )
|
||||
{
|
||||
( Some( desktop ), _ ) =>
|
||||
@@ -296,6 +302,18 @@ impl<A: App> AppData<A>
|
||||
{
|
||||
if let Some( msg ) = self.app.on_toplevel_event( event ) { self.pending_msgs.push( msg ); }
|
||||
}
|
||||
PageEvent::FilesChanged( dir ) =>
|
||||
{
|
||||
if let Some( msg ) = self.app.on_files_changed( &dir ) { self.pending_msgs.push( msg ); }
|
||||
}
|
||||
PageEvent::Mounts( mounts ) =>
|
||||
{
|
||||
if let Some( msg ) = self.app.on_mounts_changed( &mounts ) { self.pending_msgs.push( msg ); }
|
||||
}
|
||||
PageEvent::OpenFiles( paths ) =>
|
||||
{
|
||||
if let Some( msg ) = self.app.on_open_files( paths ) { self.pending_msgs.push( msg ); }
|
||||
}
|
||||
}
|
||||
}
|
||||
self.dispatch_sources();
|
||||
|
||||
+21
-15
@@ -18,36 +18,42 @@ thread_local!
|
||||
/// Send `message`, one of the shell protocol's JSON objects, to forge/web.
|
||||
pub fn shell_send( message: &str )
|
||||
{
|
||||
let message: Value = match serde_json::from_str( message )
|
||||
{
|
||||
Ok( m ) => m,
|
||||
Err( e ) =>
|
||||
{
|
||||
web_sys::console::warn_1( &format!( "[ltk] shell message is not JSON ({e}): {message}" ).into() );
|
||||
return;
|
||||
}
|
||||
};
|
||||
let Some( message ) = parse( message ) else { return };
|
||||
let request = REQUEST.with( |r| r.borrow().clone() );
|
||||
match request
|
||||
{
|
||||
Some( request ) => send( &request, message ),
|
||||
Some( request ) => { send( &request, message ); }
|
||||
None => PENDING.with( |p| p.borrow_mut().push( message ) ),
|
||||
}
|
||||
}
|
||||
|
||||
/// Send `message` as [`shell_send`] does and return forge/web's reply, as JSON, at once. `None` before the app has connected, since a request is not kept for later, and when forge/web replies nothing.
|
||||
pub fn shell_request( message: &str ) -> Option<String>
|
||||
{
|
||||
let message = parse( message )?;
|
||||
let request = REQUEST.with( |r| r.borrow().clone() )?;
|
||||
send( &request, message )?.as_string()
|
||||
}
|
||||
|
||||
fn parse( message: &str ) -> Option<Value>
|
||||
{
|
||||
serde_json::from_str( message )
|
||||
.map_err( |e| web_sys::console::warn_1( &format!( "[ltk] shell message is not JSON ({e}): {message}" ).into() ) )
|
||||
.ok()
|
||||
}
|
||||
|
||||
/// Have `listener` receive each of forge/web's shell messages as JSON.
|
||||
pub fn set_shell_listener( listener: impl Fn( &str ) + 'static )
|
||||
{
|
||||
LISTENER.with( |l| *l.borrow_mut() = Some( Box::new( listener ) ) );
|
||||
}
|
||||
|
||||
fn send( request: &js_sys::Function, message: Value )
|
||||
fn send( request: &js_sys::Function, message: Value ) -> Option<JsValue>
|
||||
{
|
||||
let json = json!( { "type": "shell", "message": message } ).to_string();
|
||||
if let Err( e ) = request.call1( &JsValue::NULL, &JsValue::from_str( &json ) )
|
||||
{
|
||||
web_sys::console::warn_2( &"[ltk] forge/web refused a shell message:".into(), &e );
|
||||
}
|
||||
request.call1( &JsValue::NULL, &JsValue::from_str( &json ) )
|
||||
.map_err( |e| web_sys::console::warn_2( &"[ltk] forge/web refused a shell message:".into(), &e ) )
|
||||
.ok()
|
||||
}
|
||||
|
||||
/// The connection's `request`, once connected, and `None` once it has closed.
|
||||
|
||||
Reference in New Issue
Block a user