///|
fn[T] HostServicesAdapter::service_task(
self : HostServicesAdapter,
key~ : String,
label~ : String,
request~ : @backend_host.HostServiceRequest,
decode~ : (@backend_host.HostServiceResponse) -> @app_services.ServiceTaskResult[
T,
],
) -> @app_services.ServiceTask[T] {
@app_services.ServiceTask::new(key~, label~, start=complete => {
match self.dispatch(request) {
Pending(id) => {
let active = Ref(true)
let (registered, cleanup) = self.on_completed_with_cleanup(id, completion => {
if active.val {
active.val = false
complete(decode(completion.response))
}
})
if !registered {
active.val = false
complete(
Failure(
@app_services.ServiceError::new(
code="completion-unavailable",
message="host service completion could not be registered",
),
),
)
None
} else {
Some(() => {
active.val = false
match cleanup {
Some(cancel) => cancel()
None => ()
}
})
}
}
response => {
complete(decode(response))
None
}
}
})
}
///|
fn[T] host_failure(
response : @backend_host.HostServiceResponse,
) -> @app_services.ServiceTaskResult[T] {
match response {
Unavailable(message) =>
Failure(@app_services.ServiceError::unavailable(message))
_ =>
Failure(
@app_services.ServiceError::new(
code="unexpected-response",
message="host service returned an unexpected response",
),
)
}
}
///|
fn[T] HostServicesAdapter::platform_task(
self : HostServicesAdapter,
operation~ : String,
payload~ : String,
decode~ : (@backend_host.PlatformChannelResponse) -> @app_services.ServiceTaskResult[
T,
],
) -> @app_services.ServiceTask[T] {
self.service_task(
key="platform:" + operation,
label="Platform " + operation,
request=PlatformChannel(
@backend_host.PlatformChannelRequest::new(
channel="moui.platform",
operation~,
payload~,
),
),
decode=response => {
match response {
PlatformChannel(platform_response) => decode(platform_response)
other => host_failure(other)
}
},
)
}
///|
fn platform_unit_result(
response : @backend_host.PlatformChannelResponse,
) -> @app_services.ServiceTaskResult[Unit] {
match response {
Ok(_) => Success(())
Unavailable(message) =>
Failure(@app_services.ServiceError::unavailable(message))
Error(message) =>
Failure(@app_services.ServiceError::new(code="platform-error", message~))
}
}
///|
fn platform_permission_result(
response : @backend_host.PlatformChannelResponse,
) -> @app_services.ServiceTaskResult[@app_services.PermissionStatus] {
match response {
Ok("granted") => Success(Granted)
Ok("denied") => Success(Denied)
Ok("restricted") => Success(Restricted)
Ok(_) =>
Failure(
@app_services.ServiceError::new(
code="invalid-platform-response",
message="permission service returned an unknown status",
),
)
Unavailable(message) =>
Failure(@app_services.ServiceError::unavailable(message))
Error(message) =>
Failure(@app_services.ServiceError::new(code="platform-error", message~))
}
}
///|
fn HostServicesAdapter::app_platform_services(
self : HostServicesAdapter,
lifecycle? : @app_services.AppLifecycleSource? = None,
) -> @app_services.PlatformServices {
@app_services.PlatformServices::new(
notifications=@app_services.NotificationServices::new(
notify=request => {
self.platform_task(
operation="notification.send",
payload=@json.to_json(request).stringify(),
decode=platform_unit_result,
)
},
cancel=id => {
self.platform_task(
operation="notification.cancel",
payload=@json.to_json(id).stringify(),
decode=platform_unit_result,
)
},
),
tray=@app_services.TrayServices::new(
set=item => {
self.platform_task(
operation="tray.set",
payload=@json.to_json(item).stringify(),
decode=platform_unit_result,
)
},
remove=id => {
self.platform_task(
operation="tray.remove",
payload=@json.to_json(id).stringify(),
decode=platform_unit_result,
)
},
),
permissions=@app_services.PermissionServices::new(request=kind => {
self.platform_task(
operation="permission.request",
payload=@json.to_json(kind).stringify(),
decode=platform_permission_result,
)
}),
sharing=@app_services.ShareServices::new(share=request => {
self.platform_task(
operation="share.open",
payload=@json.to_json(request).stringify(),
decode=platform_unit_result,
)
}),
printing=@app_services.PrintServices::new(print=request => {
self.platform_task(
operation="print.document",
payload=@json.to_json(request).stringify(),
decode=platform_unit_result,
)
}),
associations=@app_services.AssociationServices::new(
register_protocol=registration => {
self.platform_task(
operation="association.protocol.register",
payload=@json.to_json(registration).stringify(),
decode=platform_unit_result,
)
},
register_file=association => {
self.platform_task(
operation="association.file.register",
payload=@json.to_json(association).stringify(),
decode=platform_unit_result,
)
},
unregister=value => {
self.platform_task(
operation="association.remove",
payload=@json.to_json(value).stringify(),
decode=platform_unit_result,
)
},
),
window_state=@app_services.WindowStateServices::new(
read=window_id => {
self.platform_task(
operation="window-state.read",
payload=@json.to_json(window_id).stringify(),
decode=response => {
match response {
Ok(snapshot) => Success(Some(snapshot))
Unavailable(message) =>
Failure(@app_services.ServiceError::unavailable(message))
Error(message) =>
Failure(
@app_services.ServiceError::new(
code="platform-error",
message~,
),
)
}
},
)
},
write=(window_id, snapshot) => {
self.platform_task(
operation="window-state.write",
payload=@json.to_json({ "windowId": window_id, "snapshot": snapshot }).stringify(),
decode=platform_unit_result,
)
},
remove=window_id => {
self.platform_task(
operation="window-state.remove",
payload=@json.to_json(window_id).stringify(),
decode=platform_unit_result,
)
},
),
lifecycle~,
window_drag=self.window_drag.unwrap_or(
@app_services.WindowDragRegionSource::new(),
),
)
}
///|
fn HostServicesAdapter::app_file_services(
self : HostServicesAdapter,
) -> @app_services.FileServices {
@app_services.FileServices::new(
open_file=options => {
self.service_task(
key="file:open",
label="Open file",
request=FileDialog(
@backend_host.HostFileDialogOptions::new(
kind=OpenFile,
title=options.title,
filters=options.filters,
default_name=options.default_name,
),
),
decode=response => {
match response {
FileDialogSelection([path, ..]) => Success(path)
FileDialogSelection([]) => Cancelled
other => host_failure(other)
}
},
)
},
save_file=options => {
self.service_task(
key="file:save",
label="Save file",
request=FileDialog(
@backend_host.HostFileDialogOptions::new(
kind=SaveFile,
title=options.title,
filters=options.filters,
default_name=options.default_name,
),
),
decode=response => {
match response {
FileDialogSelection([path, ..]) => Success(path)
FileDialogSelection([]) => Cancelled
other => host_failure(other)
}
},
)
},
pick_directory=options => {
self.service_task(
key="file:pick-directory",
label="Pick directory",
request=FileDialog(
@backend_host.HostFileDialogOptions::new(
kind=PickDirectory,
title=options.title,
filters=options.filters,
default_name=options.default_name,
),
),
decode=response => {
match response {
FileDialogSelection([path, ..]) => Success(path)
FileDialogSelection([]) => Cancelled
other => host_failure(other)
}
},
)
},
read_text=path => {
self.service_task(
key="file:read-text:" + path,
label="Read text file",
request=TextFile(ReadText(path)),
decode=response => {
match response {
TextFileContent(_path, text) => Success(text)
other => host_failure(other)
}
},
)
},
write_text=(path, text) => {
self.service_task(
key="file:write-text:" + path,
label="Write text file",
request=TextFile(WriteText(path, text)),
decode=response => {
match response {
TextFileWritten(_) => Success(())
other => host_failure(other)
}
},
)
},
read_bytes=path => {
self.service_task(
key="file:read-bytes:" + path,
label="Read binary file",
request=BinaryFile(ReadBytes(path)),
decode=response => {
match response {
BinaryFileContent(_path, bytes) => Success(bytes)
other => host_failure(other)
}
},
)
},
write_bytes=(path, bytes) => {
self.service_task(
key="file:write-bytes:" + path,
label="Write binary file",
request=BinaryFile(WriteBytes(path, bytes)),
decode=response => {
match response {
BinaryFileWritten(_) => Success(())
other => host_failure(other)
}
},
)
},
list_directory=path => {
self.service_task(
key="file:list-directory:" + path,
label="List directory",
request=ListDirectory(path),
decode=response => {
match response {
DirectoryListing(_path, entries) =>
Success(
entries.map(entry => {
name: entry.name(),
path: entry.path(),
is_directory: entry.is_directory(),
}),
)
other => host_failure(other)
}
},
)
},
delete_file=path => {
self.service_task(
key="file:delete:" + path,
label="Delete file",
request=DeleteFile(path),
decode=response => {
match response {
FileDeleted(_path) => Success(())
other => host_failure(other)
}
},
)
},
)
}
///|
fn HostServicesAdapter::app_clipboard_services(
self : HostServicesAdapter,
) -> @app_services.ClipboardServices {
@app_services.ClipboardServices::new(
read_text=() => {
self.service_task(
key="clipboard:read-text",
label="Read clipboard text",
request=Clipboard(ReadText),
decode=response => {
match response {
ClipboardText(text) => Success(text)
other => host_failure(other)
}
},
)
},
write_text=text => {
self.service_task(
key="clipboard:write-text",
label="Write clipboard text",
request=Clipboard(WriteText(text)),
decode=response => {
match response {
ClipboardWritten => Success(())
other => host_failure(other)
}
},
)
},
read_image=() => {
self.service_task(
key="clipboard:read-image",
label="Read clipboard image",
request=Clipboard(ReadImage),
decode=response => {
match response {
ClipboardImage(bytes) =>
Success({ mime_type: "application/octet-stream", bytes, })
other => host_failure(other)
}
},
)
},
write_image=image => {
self.service_task(
key="clipboard:write-image",
label="Write clipboard image",
request=Clipboard(WriteImage(image.mime_type, image.bytes)),
decode=response => {
match response {
ClipboardWritten => Success(())
other => host_failure(other)
}
},
)
},
)
}
///|
fn app_menu_to_host(
menus : Array[@app_services.ApplicationMenu],
) -> Array[@backend_host.HostApplicationMenu] {
let next_action_id = Ref(1)
menus.map(menu => {
@backend_host.HostApplicationMenu::new(
title=menu.title,
placement=match menu.placement {
MenuBar => MenuBar
ApplicationMenu => ApplicationMenu
},
items=menu.items.map(item => {
match item {
MenuSeparator => @backend_host.HostApplicationMenuItem::separator()
MenuCommand(command) => {
let action_id = next_action_id.val
next_action_id.val = action_id + 1
@backend_host.HostApplicationMenuItem::new(
title=command.display_label(),
action_id~,
enabled=command.enabled,
)
}
}
}),
)
})
}
///|
fn HostServicesAdapter::app_menu_services(
self : HostServicesAdapter,
) -> @app_services.MenuServices {
@app_services.MenuServices::new(
show_context=commands => {
self.service_task(
key="menu:context",
label="Show context menu",
request=ShowMenu(commands),
decode=response => {
match response {
MenuAction(command) => Success(command)
MenuCommand(intent) =>
match
commands.iter().find_first(command => command.intent == intent) {
Some(command) => Success(command)
None => host_failure(response)
}
other => host_failure(other)
}
},
)
},
install_application=menus => {
self.service_task(
key="menu:application",
label="Install application menu",
request=SetApplicationMenu(app_menu_to_host(menus)),
decode=response => {
match response {
ApplicationMenuInstalled => Success(())
other => host_failure(other)
}
},
)
},
)
}
///|
fn HostServicesAdapter::as_app_services(
self : HostServicesAdapter,
lifecycle? : @app_services.AppLifecycleSource? = None,
) -> @app_services.AppServices {
@app_services.AppServices::new(
files=self.app_file_services(),
clipboard=self.app_clipboard_services(),
urls=@app_services.UrlServices::new(open=url => {
self.service_task(
key="url:open",
label="Open URL",
request=OpenUrl(url),
decode=response => {
match response {
UrlOpened => Success(())
other => host_failure(other)
}
},
)
}),
settings=@app_services.SettingsServices::new(
read=key => {
self.service_task(
key="settings:read:" + key,
label="Read setting",
request=Settings(Read(key)),
decode=response => {
match response {
SettingValue(value) => Success(value)
other => host_failure(other)
}
},
)
},
write=(key, value) => {
self.service_task(
key="settings:write:" + key,
label="Write setting",
request=Settings(Write(key, value)),
decode=response => {
match response {
SettingWritten => Success(())
other => host_failure(other)
}
},
)
},
remove=key => {
self.service_task(
key="settings:remove:" + key,
label="Remove setting",
request=Settings(Remove(key)),
decode=response => {
match response {
SettingRemoved => Success(())
other => host_failure(other)
}
},
)
},
),
appearance=@app_services.AppearanceServices::new(system_color_scheme=() => {
self.service_task(
key="appearance:system-color-scheme",
label="Read system color scheme",
request=QuerySystemTheme,
decode=response => {
match response {
SystemTheme(scheme) => Success(scheme)
other => host_failure(other)
}
},
)
}),
menus=self.app_menu_services(),
platform=self.app_platform_services(lifecycle~),
)
}
///|
fn HostServicesAdapter::app_environment(
self : HostServicesAdapter,
timer? : @app_services.TimerSource? = None,
routes? : @app_services.RouteSource? = None,
lifecycle? : @app_services.AppLifecycleSource? = None,
clock? : (() -> Double)? = None,
) -> @app_services.AppEnvironment {
@app_services.AppEnvironment::new(
services=self.as_app_services(lifecycle~),
timer~,
routes~,
clock~,
)
}
///|
/// Adapt the host wire protocol to the typed app-facing services boundary.
/// Request ids and completion queues remain private to this package.
pub fn app_services(
bridge~ : @backend_host.HostServiceBridge,
async_queue? : ServiceAsyncQueue? = None,
lifecycle? : @app_services.AppLifecycleSource? = None,
window_drag? : @app_services.WindowDragRegionSource? = None,
) -> @app_services.AppServices {
HostServicesAdapter::new(bridge~, async_queue~, window_drag~).as_app_services(
lifecycle~,
)
}
///|
/// Build the app-facing environment supplied to a Program closure.
pub fn app_environment(
bridge~ : @backend_host.HostServiceBridge,
async_queue? : ServiceAsyncQueue? = None,
timer? : @app_services.TimerSource? = None,
routes? : @app_services.RouteSource? = None,
lifecycle? : @app_services.AppLifecycleSource? = None,
clock? : (() -> Double)? = None,
window_drag? : @app_services.WindowDragRegionSource? = None,
) -> @app_services.AppEnvironment {
HostServicesAdapter::new(bridge~, async_queue~, window_drag~).app_environment(
timer~,
routes~,
lifecycle~,
clock~,
)
}