feat(ironrdp-web): iron-remote-desktop helper crate for remote desktop WASM modules (#755)

This commit is contained in:
Alex Yusiuk
2025-04-18 07:30:45 -04:00
committed by GitHub
parent 178670b4a8
commit ec1832bba0
17 changed files with 747 additions and 251 deletions
Generated
+13 -3
View File
@@ -2329,6 +2329,18 @@ version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
[[package]]
name = "iron-remote-desktop"
version = "0.1.0"
dependencies = [
"console_error_panic_hook",
"tracing",
"tracing-subscriber",
"tracing-web",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "ironrdp"
version = "0.9.1"
@@ -2798,12 +2810,12 @@ dependencies = [
"anyhow",
"base64",
"chrono",
"console_error_panic_hook",
"futures-channel",
"futures-util",
"getrandom 0.2.15",
"gloo-net",
"gloo-timers",
"iron-remote-desktop",
"ironrdp",
"ironrdp-cliprdr-format",
"ironrdp-core",
@@ -2821,8 +2833,6 @@ dependencies = [
"tap",
"time",
"tracing",
"tracing-subscriber",
"tracing-web",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
+32
View File
@@ -0,0 +1,32 @@
[package]
name = "iron-remote-desktop"
version = "0.1.0"
edition.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
authors.workspace = true
keywords.workspace = true
categories.workspace = true
[features]
panic_hook = ["dep:console_error_panic_hook"]
[dependencies]
# WASM
wasm-bindgen = "0.2"
web-sys = { version = "0.3", features = ["HtmlCanvasElement"] }
tracing-web = "0.1"
# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
# code size when deploying.
console_error_panic_hook = { version = "0.1", optional = true }
# Logging
tracing = { version = "0.1", features = ["log"] }
tracing-subscriber = { version = "0.3", features = ["time"] }
[lints]
workspace = true
@@ -0,0 +1,18 @@
use wasm_bindgen::JsValue;
use web_sys::js_sys;
pub trait ClipboardTransaction {
type ClipboardContent: ClipboardContent;
fn init() -> Self;
fn add_content(&mut self, content: Self::ClipboardContent);
fn is_empty(&self) -> bool;
fn contents(&self) -> js_sys::Array;
}
pub trait ClipboardContent {
fn new_text(mime_type: &str, text: &str) -> Self;
fn new_binary(mime_type: &str, binary: &[u8]) -> Self;
fn mime_type(&self) -> &str;
fn value(&self) -> JsValue;
}
+10
View File
@@ -0,0 +1,10 @@
#[derive(Debug)]
pub enum CursorStyle {
Default,
Hidden,
Url {
data: String,
hotspot_x: u16,
hotspot_y: u16,
},
}
+23
View File
@@ -0,0 +1,23 @@
use wasm_bindgen::prelude::*;
pub trait IronError {
fn backtrace(&self) -> String;
fn kind(&self) -> IronErrorKind;
}
#[derive(Clone, Copy)]
#[wasm_bindgen]
pub enum IronErrorKind {
/// Catch-all error kind
General,
/// Incorrect password used
WrongPassword,
/// Unable to login to machine
LogonFailure,
/// Insufficient permission, server denied access
AccessDenied,
/// Something wrong happened when sending or receiving the RDCleanPath message
RDCleanPath,
/// Couldnt connect to proxy
ProxyConnect,
}
+17
View File
@@ -0,0 +1,17 @@
pub trait DeviceEvent {
fn mouse_button_pressed(button: u8) -> Self;
fn mouse_button_released(button: u8) -> Self;
fn mouse_move(x: u16, y: u16) -> Self;
fn wheel_rotations(vertical: bool, rotation_units: i16) -> Self;
fn key_pressed(scancode: u16) -> Self;
fn key_released(scancode: u16) -> Self;
fn unicode_pressed(unicode: char) -> Self;
fn unicode_released(unicode: char) -> Self;
}
pub trait InputTransaction {
type DeviceEvent: DeviceEvent;
fn init() -> Self;
fn add_event(&mut self, event: Self::DeviceEvent);
}
+366
View File
@@ -0,0 +1,366 @@
mod clipboard;
mod cursor;
mod error;
mod input;
mod session;
pub use clipboard::{ClipboardContent, ClipboardTransaction};
pub use cursor::CursorStyle;
pub use error::{IronError, IronErrorKind};
pub use input::{DeviceEvent, InputTransaction};
pub use session::{Session, SessionBuilder, SessionTerminationInfo};
use wasm_bindgen::prelude::wasm_bindgen;
#[wasm_bindgen]
#[derive(Clone, Copy)]
pub struct DesktopSize {
pub width: u16,
pub height: u16,
}
#[wasm_bindgen]
impl DesktopSize {
pub fn init(width: u16, height: u16) -> Self {
DesktopSize { width, height }
}
}
pub fn iron_init(log_level: &str) {
// When the `console_error_panic_hook` feature is enabled, we can call the
// `set_panic_hook` function at least once during initialization, and then
// we will get better error messages if our code ever panics.
//
// For more details see
// https://github.com/rustwasm/console_error_panic_hook#readme
#[cfg(feature = "panic_hook")]
console_error_panic_hook::set_once();
if let Ok(level) = log_level.parse::<tracing::Level>() {
set_logger_once(level);
}
}
fn set_logger_once(level: tracing::Level) {
use tracing_subscriber::filter::LevelFilter;
use tracing_subscriber::fmt::time::UtcTime;
use tracing_subscriber::prelude::*;
use tracing_web::MakeConsoleWriter;
static INIT: std::sync::Once = std::sync::Once::new();
INIT.call_once(|| {
let fmt_layer = tracing_subscriber::fmt::layer()
.with_ansi(false)
.with_timer(UtcTime::rfc_3339()) // std::time is not available in browsers
.with_writer(MakeConsoleWriter);
let level_filter = LevelFilter::from_level(level);
tracing_subscriber::registry().with(fmt_layer).with(level_filter).init();
})
}
pub trait RemoteDesktopApi {
type Session: Session;
type SessionBuilder: SessionBuilder;
type SessionTerminationInfo: SessionTerminationInfo;
type DeviceEvent: DeviceEvent;
type InputTransaction: InputTransaction;
type ClipboardTransaction: ClipboardTransaction;
type ClipboardContent: ClipboardContent;
type Error: IronError;
/// Called before the logger is set.
fn pre_init() {}
/// Called after the logger is set.
fn post_init() {}
}
#[macro_export]
macro_rules! export {
($api:ty) => {
mod __wasm_ffi {
use wasm_bindgen::prelude::*;
use web_sys::{js_sys, HtmlCanvasElement};
use $crate::{
ClipboardContent as _, ClipboardTransaction as _, DeviceEvent as _, InputTransaction as _,
IronError as _, RemoteDesktopApi, Session as _, SessionBuilder as _, SessionTerminationInfo as _,
};
#[wasm_bindgen]
pub fn iron_init(log_level: &str) {
<$api as RemoteDesktopApi>::pre_init();
$crate::iron_init(log_level);
<$api as RemoteDesktopApi>::post_init();
}
#[wasm_bindgen]
pub struct DeviceEvent(<$api as RemoteDesktopApi>::DeviceEvent);
#[wasm_bindgen]
impl DeviceEvent {
pub fn mouse_button_pressed(button: u8) -> Self {
Self(<<$api as RemoteDesktopApi>::DeviceEvent>::mouse_button_pressed(
button,
))
}
pub fn mouse_button_released(button: u8) -> Self {
Self(<<$api as RemoteDesktopApi>::DeviceEvent>::mouse_button_released(
button,
))
}
pub fn mouse_move(x: u16, y: u16) -> Self {
Self(<<$api as RemoteDesktopApi>::DeviceEvent>::mouse_move(x, y))
}
pub fn wheel_rotations(vertical: bool, rotation_units: i16) -> Self {
Self(<<$api as RemoteDesktopApi>::DeviceEvent>::wheel_rotations(
vertical,
rotation_units,
))
}
pub fn key_pressed(scancode: u16) -> Self {
Self(<<$api as RemoteDesktopApi>::DeviceEvent>::key_pressed(scancode))
}
pub fn key_released(scancode: u16) -> Self {
Self(<<$api as RemoteDesktopApi>::DeviceEvent>::key_released(scancode))
}
pub fn unicode_pressed(unicode: char) -> Self {
Self(<<$api as RemoteDesktopApi>::DeviceEvent>::unicode_pressed(
unicode,
))
}
pub fn unicode_released(unicode: char) -> Self {
Self(<<$api as RemoteDesktopApi>::DeviceEvent>::unicode_released(
unicode,
))
}
}
#[wasm_bindgen]
pub struct InputTransaction(<$api as RemoteDesktopApi>::InputTransaction);
#[wasm_bindgen]
impl InputTransaction {
pub fn init() -> Self {
Self(<<$api as RemoteDesktopApi>::InputTransaction>::init())
}
pub fn add_event(&mut self, event: DeviceEvent) {
self.0.add_event(event.0);
}
}
#[wasm_bindgen]
pub struct IronError(<$api as RemoteDesktopApi>::Error);
#[wasm_bindgen]
impl IronError {
pub fn backtrace(&self) -> String {
self.0.backtrace()
}
pub fn kind(&self) -> $crate::IronErrorKind {
self.0.kind()
}
}
#[wasm_bindgen]
pub struct Session(<$api as RemoteDesktopApi>::Session);
#[wasm_bindgen]
impl Session {
pub async fn run(&self) -> Result<SessionTerminationInfo, IronError> {
self.0.run().await.map(SessionTerminationInfo).map_err(IronError)
}
pub fn desktop_size(&self) -> $crate::DesktopSize {
self.0.desktop_size()
}
pub fn apply_inputs(&self, transaction: InputTransaction) -> Result<(), IronError> {
self.0.apply_inputs(transaction.0).map_err(IronError)
}
pub fn release_all_inputs(&self) -> Result<(), IronError> {
self.0.release_all_inputs().map_err(IronError)
}
pub fn synchronize_lock_keys(
&self,
scroll_lock: bool,
num_lock: bool,
caps_lock: bool,
kana_lock: bool,
) -> Result<(), IronError> {
self.0
.synchronize_lock_keys(scroll_lock, num_lock, caps_lock, kana_lock)
.map_err(IronError)
}
pub fn shutdown(&self) -> Result<(), IronError> {
self.0.shutdown().map_err(IronError)
}
pub async fn on_clipboard_paste(&self, content: ClipboardTransaction) -> Result<(), IronError> {
self.0.on_clipboard_paste(content.0).await.map_err(IronError)
}
pub fn resize(
&self,
width: u32,
height: u32,
scale_factor: Option<u32>,
physical_width: Option<u32>,
physical_height: Option<u32>,
) {
self.0
.resize(width, height, scale_factor, physical_width, physical_height);
}
pub fn supports_unicode_keyboard_shortcuts(&self) -> bool {
self.0.supports_unicode_keyboard_shortcuts()
}
pub fn extension_call(value: JsValue) -> Result<JsValue, IronError> {
<<$api as RemoteDesktopApi>::Session>::extension_call(value).map_err(IronError)
}
}
#[wasm_bindgen]
pub struct SessionBuilder(<$api as RemoteDesktopApi>::SessionBuilder);
#[wasm_bindgen]
impl SessionBuilder {
pub fn init() -> Self {
Self(<<$api as RemoteDesktopApi>::SessionBuilder>::init())
}
pub fn username(&self, username: String) -> Self {
Self(self.0.username(username))
}
pub fn destination(&self, destination: String) -> Self {
Self(self.0.destination(destination))
}
pub fn server_domain(&self, server_domain: String) -> Self {
Self(self.0.server_domain(server_domain))
}
pub fn password(&self, password: String) -> Self {
Self(self.0.password(password))
}
pub fn proxy_address(&self, address: String) -> Self {
Self(self.0.proxy_address(address))
}
pub fn auth_token(&self, token: String) -> Self {
Self(self.0.auth_token(token))
}
pub fn desktop_size(&self, desktop_size: $crate::DesktopSize) -> Self {
Self(self.0.desktop_size(desktop_size))
}
pub fn render_canvas(&self, canvas: HtmlCanvasElement) -> Self {
Self(self.0.render_canvas(canvas))
}
pub fn set_cursor_style_callback(&self, callback: js_sys::Function) -> Self {
Self(self.0.set_cursor_style_callback(callback))
}
pub fn set_cursor_style_callback_context(&self, context: JsValue) -> Self {
Self(self.0.set_cursor_style_callback_context(context))
}
pub fn remote_clipboard_changed_callback(&self, callback: js_sys::Function) -> Self {
Self(self.0.remote_clipboard_changed_callback(callback))
}
pub fn remote_received_format_list_callback(&self, callback: js_sys::Function) -> Self {
Self(self.0.remote_received_format_list_callback(callback))
}
pub fn force_clipboard_update_callback(&self, callback: js_sys::Function) -> Self {
Self(self.0.force_clipboard_update_callback(callback))
}
pub fn extension(&self, value: JsValue) -> Self {
Self(self.0.extension(value))
}
pub async fn connect(&self) -> Result<Session, IronError> {
self.0.connect().await.map(Session).map_err(IronError)
}
}
#[wasm_bindgen]
pub struct SessionTerminationInfo(<$api as RemoteDesktopApi>::SessionTerminationInfo);
#[wasm_bindgen]
impl SessionTerminationInfo {
pub fn reason(&self) -> String {
self.0.reason()
}
}
#[wasm_bindgen]
pub struct ClipboardTransaction(<$api as RemoteDesktopApi>::ClipboardTransaction);
#[wasm_bindgen]
impl ClipboardTransaction {
pub fn init() -> Self {
Self(<<$api as RemoteDesktopApi>::ClipboardTransaction>::init())
}
pub fn add_content(&mut self, content: ClipboardContent) {
self.0.add_content(content.0);
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn content(&self) -> js_sys::Array {
iron_remote_desktop::ClipboardTransaction::contents(&self.0)
}
}
#[wasm_bindgen]
pub struct ClipboardContent(<$api as RemoteDesktopApi>::ClipboardContent);
#[wasm_bindgen]
impl ClipboardContent {
pub fn new_text(mime_type: &str, text: &str) -> Self {
Self(<<$api as RemoteDesktopApi>::ClipboardContent>::new_text(
mime_type, text,
))
}
pub fn new_binary(mime_type: &str, binary: &[u8]) -> Self {
Self(<<$api as RemoteDesktopApi>::ClipboardContent>::new_binary(
mime_type, binary,
))
}
pub fn mime_type(&self) -> String {
self.0.mime_type().to_owned()
}
pub fn value(&self) -> JsValue {
iron_remote_desktop::ClipboardContent::value(&self.0)
}
}
}
};
}
+82
View File
@@ -0,0 +1,82 @@
use wasm_bindgen::JsValue;
use web_sys::{js_sys, HtmlCanvasElement};
use crate::clipboard::ClipboardTransaction;
use crate::error::IronError;
use crate::input::InputTransaction;
use crate::DesktopSize;
pub trait SessionBuilder {
type Session: Session;
type Error: IronError;
fn init() -> Self;
#[must_use]
fn username(&self, username: String) -> Self;
#[must_use]
fn destination(&self, destination: String) -> Self;
#[must_use]
fn server_domain(&self, server_domain: String) -> Self;
#[must_use]
fn password(&self, password: String) -> Self;
#[must_use]
fn proxy_address(&self, address: String) -> Self;
#[must_use]
fn auth_token(&self, token: String) -> Self;
#[must_use]
fn desktop_size(&self, desktop_size: DesktopSize) -> Self;
#[must_use]
fn render_canvas(&self, canvas: HtmlCanvasElement) -> Self;
#[must_use]
fn set_cursor_style_callback(&self, callback: js_sys::Function) -> Self;
#[must_use]
fn set_cursor_style_callback_context(&self, context: JsValue) -> Self;
#[must_use]
fn remote_clipboard_changed_callback(&self, callback: js_sys::Function) -> Self;
#[must_use]
fn remote_received_format_list_callback(&self, callback: js_sys::Function) -> Self;
#[must_use]
fn force_clipboard_update_callback(&self, callback: js_sys::Function) -> Self;
#[must_use]
fn extension(&self, value: JsValue) -> Self;
#[expect(async_fn_in_trait)]
async fn connect(&self) -> Result<Self::Session, Self::Error>;
}
pub trait Session {
type SessionTerminationInfo: SessionTerminationInfo;
type InputTransaction: InputTransaction;
type ClipboardTransaction: ClipboardTransaction;
type Error: IronError;
fn run(&self) -> impl core::future::Future<Output = Result<Self::SessionTerminationInfo, Self::Error>>;
fn desktop_size(&self) -> DesktopSize;
fn apply_inputs(&self, transaction: Self::InputTransaction) -> Result<(), Self::Error>;
fn release_all_inputs(&self) -> Result<(), Self::Error>;
fn synchronize_lock_keys(
&self,
scroll_lock: bool,
num_lock: bool,
caps_lock: bool,
kana_lock: bool,
) -> Result<(), Self::Error>;
fn shutdown(&self) -> Result<(), Self::Error>;
fn on_clipboard_paste(
&self,
content: Self::ClipboardTransaction,
) -> impl core::future::Future<Output = Result<(), Self::Error>>;
fn resize(
&self,
width: u32,
height: u32,
scale_factor: Option<u32>,
physical_width: Option<u32>,
physical_height: Option<u32>,
);
fn supports_unicode_keyboard_shortcuts(&self) -> bool;
fn extension_call(value: JsValue) -> Result<JsValue, Self::Error>;
}
pub trait SessionTerminationInfo {
fn reason(&self) -> String;
}
+2 -9
View File
@@ -19,7 +19,7 @@ crate-type = ["cdylib", "rlib"]
[features]
default = ["panic_hook"]
panic_hook = ["dep:console_error_panic_hook"]
panic_hook = ["iron-remote-desktop/panic_hook"]
[dependencies]
# Protocols
@@ -37,6 +37,7 @@ ironrdp-core.path = "../ironrdp-core"
ironrdp-cliprdr-format.path = "../ironrdp-cliprdr-format"
ironrdp-futures.path = "../ironrdp-futures"
ironrdp-rdcleanpath.path = "../ironrdp-rdcleanpath"
iron-remote-desktop.path = "../iron-remote-desktop"
# WASM
wasm-bindgen = "0.2"
@@ -47,7 +48,6 @@ gloo-net = { version = "0.6", default-features = false, features = ["websocket",
gloo-timers = { version = "0.3", default-features = false, features = ["futures"] }
serde = { version = "1.0", features = ["derive"] }
serde-wasm-bindgen = "0.6"
tracing-web = "0.1"
# Rendering
softbuffer = { version = "0.4", default-features = false }
@@ -60,19 +60,12 @@ getrandom = { version = "0.2", features = ["js"] }
chrono = { version = "0.4", features = ["wasmbind"] }
time = { version = "0.3", features = ["wasm-bindgen"] }
# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
# code size when deploying.
console_error_panic_hook = { version = "0.1", optional = true }
# Async
futures-util = { version = "0.3", features = ["sink", "io"] }
futures-channel = "0.3"
# Logging
tracing = { version = "0.1", features = ["log"] }
tracing-subscriber = { version = "0.3", features = ["time"] }
# Utils
anyhow = "1"
+17 -15
View File
@@ -16,6 +16,7 @@ mod transaction;
use std::collections::HashMap;
use futures_channel::mpsc;
use iron_remote_desktop::{ClipboardContent as _, ClipboardTransaction as _};
use ironrdp::cliprdr::backend::{ClipboardMessage, CliprdrBackend};
use ironrdp::cliprdr::pdu::{
ClipboardFormat, ClipboardFormatId, ClipboardFormatName, ClipboardGeneralCapabilityFlags, FileContentsRequest,
@@ -24,13 +25,13 @@ use ironrdp::cliprdr::pdu::{
use ironrdp_cliprdr_format::bitmap::{dib_to_png, dibv5_to_png, png_to_cf_dibv5};
use ironrdp_cliprdr_format::html::{cf_html_to_plain_html, plain_html_to_cf_html};
use ironrdp_core::{impl_as_any, IntoOwned};
use transaction::{ClipboardContent, ClipboardContentValue};
use transaction::ClipboardContentValue;
use wasm_bindgen::prelude::*;
use crate::session::RdpInputEvent;
#[rustfmt::skip]
pub(crate) use transaction::ClipboardTransaction;
pub(crate) use transaction::{RdpClipboardTransaction, RdpClipboardContent};
const MIME_TEXT: &str = "text/plain";
const MIME_HTML: &str = "text/html";
@@ -103,7 +104,7 @@ impl WasmClipboardMessageProxy {
/// Messages sent by the JS code or CLIPRDR to the backend implementation.
#[derive(Debug)]
pub(crate) enum WasmClipboardBackendMessage {
LocalClipboardChanged(ClipboardTransaction),
LocalClipboardChanged(RdpClipboardTransaction),
RemoteDataRequest(ClipboardFormatId),
RemoteClipboardChanged(Vec<ClipboardFormat>),
@@ -116,8 +117,8 @@ pub(crate) enum WasmClipboardBackendMessage {
/// Clipboard backend implementation for web. This object should be created once per session and
/// kept alive until session is terminated.
pub(crate) struct WasmClipboard {
local_clipboard: Option<ClipboardTransaction>,
remote_clipboard: ClipboardTransaction,
local_clipboard: Option<RdpClipboardTransaction>,
remote_clipboard: RdpClipboardTransaction,
remote_mapping: HashMap<ClipboardFormatId, String>,
remote_formats_to_read: Vec<ClipboardFormatId>,
@@ -137,7 +138,7 @@ impl WasmClipboard {
pub(crate) fn new(message_proxy: WasmClipboardMessageProxy, js_callbacks: JsClipboardCallbacks) -> Self {
Self {
local_clipboard: None,
remote_clipboard: ClipboardTransaction::init(),
remote_clipboard: RdpClipboardTransaction::init(),
proxy: message_proxy,
js_callbacks,
@@ -155,7 +156,7 @@ impl WasmClipboard {
fn handle_local_clipboard_changed(
&mut self,
transaction: ClipboardTransaction,
transaction: RdpClipboardTransaction,
) -> anyhow::Result<Vec<ClipboardFormat>> {
let mut formats = Vec::new();
transaction.contents().iter().for_each(|content| {
@@ -371,21 +372,21 @@ impl WasmClipboard {
let content = match pending_format {
ClipboardFormatId::CF_UNICODETEXT => match response.to_unicode_string() {
Ok(text) => Some(ClipboardContent::new_text(MIME_TEXT, &text)),
Ok(text) => Some(RdpClipboardContent::new_text(MIME_TEXT, &text)),
Err(err) => {
error!("CF_UNICODETEXT decode error: {}", err);
None
}
},
ClipboardFormatId::CF_DIB => match dib_to_png(response.data()) {
Ok(png) => Some(ClipboardContent::new_binary(MIME_PNG, &png)),
Ok(png) => Some(RdpClipboardContent::new_binary(MIME_PNG, &png)),
Err(err) => {
warn!("DIB decode error: {}", err);
None
}
},
ClipboardFormatId::CF_DIBV5 => match dibv5_to_png(response.data()) {
Ok(png) => Some(ClipboardContent::new_binary(MIME_PNG, &png)),
Ok(png) => Some(RdpClipboardContent::new_binary(MIME_PNG, &png)),
Err(err) => {
warn!("DIBv5 decode error: {}", err);
None
@@ -395,21 +396,21 @@ impl WasmClipboard {
let format_name = self.remote_mapping.get(&registered).map(|s| s.as_str());
match format_name {
Some(FORMAT_WIN_HTML_NAME) => match cf_html_to_plain_html(response.data()) {
Ok(text) => Some(ClipboardContent::new_text(MIME_HTML, text)),
Ok(text) => Some(RdpClipboardContent::new_text(MIME_HTML, text)),
Err(err) => {
warn!("CF_HTML decode error: {}", err);
None
}
},
Some(FORMAT_MIME_HTML_NAME) => match response.to_string() {
Ok(text) => Some(ClipboardContent::new_text(MIME_HTML, &text)),
Ok(text) => Some(RdpClipboardContent::new_text(MIME_HTML, &text)),
Err(err) => {
warn!("text/html decode error: {}", err);
None
}
},
Some(FORMAT_MIME_PNG_NAME) | Some(FORMAT_PNG_NAME) => {
Some(ClipboardContent::new_binary(MIME_PNG, response.data()))
Some(RdpClipboardContent::new_binary(MIME_PNG, response.data()))
}
_ => {
// Not supported format
@@ -433,11 +434,12 @@ impl WasmClipboard {
if transaction.is_empty() {
return Ok(());
}
// Set clipboard when all formats were read
self.js_callbacks
.on_remote_clipboard_changed
.call1(&JsValue::NULL, &JsValue::from(transaction))
.expect("Failed to call JS callback");
.expect("failed to call JS callback");
}
Ok(())
@@ -505,7 +507,7 @@ impl WasmClipboard {
} else {
// If no initial clipboard callback was set, send empty format list instead
return self.process_event(WasmClipboardBackendMessage::LocalClipboardChanged(
ClipboardTransaction::init(),
RdpClipboardTransaction::init(),
));
}
}
+37 -39
View File
@@ -1,48 +1,49 @@
use wasm_bindgen::prelude::*;
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::JsValue;
/// Object which represents complete clipboard transaction with multiple MIME types.
#[wasm_bindgen]
#[derive(Debug, Default, Clone)]
pub struct ClipboardTransaction {
contents: Vec<ClipboardContent>,
pub(crate) struct RdpClipboardTransaction {
contents: Vec<RdpClipboardContent>,
}
impl ClipboardTransaction {
pub fn contents(&self) -> &[ClipboardContent] {
impl RdpClipboardTransaction {
pub(crate) fn contents(&self) -> &[RdpClipboardContent] {
&self.contents
}
pub fn clear(&mut self) {
pub(crate) fn clear(&mut self) {
self.contents.clear();
}
}
#[wasm_bindgen]
impl ClipboardTransaction {
pub fn init() -> Self {
impl iron_remote_desktop::ClipboardTransaction for RdpClipboardTransaction {
type ClipboardContent = RdpClipboardContent;
fn init() -> Self {
Self { contents: Vec::new() }
}
pub fn add_content(&mut self, content: ClipboardContent) {
fn add_content(&mut self, content: Self::ClipboardContent) {
self.contents.push(content);
}
pub fn is_empty(&self) -> bool {
fn is_empty(&self) -> bool {
self.contents.is_empty()
}
#[wasm_bindgen(js_name = content)]
pub fn js_contents(&self) -> js_sys::Array {
fn contents(&self) -> js_sys::Array {
js_sys::Array::from_iter(
self.contents
.iter()
.map(|content: &ClipboardContent| JsValue::from(content.clone())),
.map(|content: &RdpClipboardContent| JsValue::from(content.clone())),
)
}
}
impl FromIterator<ClipboardContent> for ClipboardTransaction {
fn from_iter<T: IntoIterator<Item = ClipboardContent>>(iter: T) -> Self {
impl FromIterator<RdpClipboardContent> for RdpClipboardTransaction {
fn from_iter<T: IntoIterator<Item = RdpClipboardContent>>(iter: T) -> Self {
Self {
contents: iter.into_iter().collect(),
}
@@ -50,13 +51,13 @@ impl FromIterator<ClipboardContent> for ClipboardTransaction {
}
#[derive(Debug, Clone)]
pub enum ClipboardContentValue {
pub(crate) enum ClipboardContentValue {
Text(String),
Binary(Vec<u8>),
}
impl ClipboardContentValue {
pub fn js_value(&self) -> JsValue {
pub(crate) fn value(&self) -> JsValue {
match self {
ClipboardContentValue::Text(text) => JsValue::from_str(text),
ClipboardContentValue::Binary(binary) => js_sys::Uint8Array::from(binary.as_slice()).into(),
@@ -67,44 +68,41 @@ impl ClipboardContentValue {
/// Object which represents single clipboard format represented standard MIME type.
#[wasm_bindgen]
#[derive(Debug, Clone)]
pub struct ClipboardContent {
pub(crate) struct RdpClipboardContent {
mime_type: String,
value: ClipboardContentValue,
}
#[wasm_bindgen]
impl ClipboardContent {
pub fn new_text(mime_type: &str, text: &str) -> Self {
impl RdpClipboardContent {
pub(crate) fn mime_type(&self) -> &str {
&self.mime_type
}
pub(crate) fn value(&self) -> &ClipboardContentValue {
&self.value
}
}
impl iron_remote_desktop::ClipboardContent for RdpClipboardContent {
fn new_text(mime_type: &str, text: &str) -> Self {
Self {
mime_type: mime_type.into(),
value: ClipboardContentValue::Text(text.to_owned()),
}
}
pub fn new_binary(mime_type: &str, binary: &[u8]) -> Self {
fn new_binary(mime_type: &str, binary: &[u8]) -> Self {
Self {
mime_type: mime_type.into(),
value: ClipboardContentValue::Binary(binary.to_vec()),
}
}
#[wasm_bindgen(js_name = mime_type)]
pub fn js_mime_type(&self) -> String {
self.mime_type.clone()
fn mime_type(&self) -> &str {
self.mime_type.as_str()
}
#[wasm_bindgen(js_name = value)]
pub fn js_value(&self) -> JsValue {
self.value.js_value()
}
}
impl ClipboardContent {
pub fn mime_type(&self) -> &str {
&self.mime_type
}
pub fn value(&self) -> &ClipboardContentValue {
&self.value
fn value(&self) -> JsValue {
self.value.value()
}
}
+6 -25
View File
@@ -1,43 +1,24 @@
use iron_remote_desktop::IronErrorKind;
use ironrdp::connector::{self, sspi, ConnectorErrorKind};
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
#[derive(Clone, Copy)]
pub enum IronErrorKind {
/// Catch-all error kind
General,
/// Incorrect password used
WrongPassword,
/// Unable to login to machine
LogonFailure,
/// Insufficient permission, server denied access
AccessDenied,
/// Something wrong happened when sending or receiving the RDCleanPath message
RDCleanPath,
/// Couldnt connect to proxy
ProxyConnect,
}
#[wasm_bindgen]
pub struct IronError {
pub(crate) struct IronError {
kind: IronErrorKind,
source: anyhow::Error,
}
impl IronError {
pub fn with_kind(mut self, kind: IronErrorKind) -> Self {
pub(crate) fn with_kind(mut self, kind: IronErrorKind) -> Self {
self.kind = kind;
self
}
}
#[wasm_bindgen]
impl IronError {
pub fn backtrace(&self) -> String {
impl iron_remote_desktop::IronError for IronError {
fn backtrace(&self) -> String {
format!("{:?}", self.source)
}
pub fn kind(&self) -> IronErrorKind {
fn kind(&self) -> IronErrorKind {
self.kind
}
}
+16 -19
View File
@@ -1,14 +1,11 @@
use ironrdp::input::{MouseButton, MousePosition, Operation, Scancode, WheelRotations};
use smallvec::SmallVec;
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
#[derive(Clone)]
pub struct DeviceEvent(pub(crate) Operation);
pub(crate) struct DeviceEvent(pub(crate) Operation);
#[wasm_bindgen]
impl DeviceEvent {
pub fn mouse_button_pressed(button: u8) -> Self {
impl iron_remote_desktop::DeviceEvent for DeviceEvent {
fn mouse_button_pressed(button: u8) -> Self {
match MouseButton::from_web_button(button) {
Some(button) => Self(Operation::MouseButtonPressed(button)),
None => {
@@ -18,7 +15,7 @@ impl DeviceEvent {
}
}
pub fn mouse_button_released(button: u8) -> Self {
fn mouse_button_released(button: u8) -> Self {
match MouseButton::from_web_button(button) {
Some(button) => Self(Operation::MouseButtonReleased(button)),
None => {
@@ -28,44 +25,44 @@ impl DeviceEvent {
}
}
pub fn mouse_move(x: u16, y: u16) -> Self {
fn mouse_move(x: u16, y: u16) -> Self {
Self(Operation::MouseMove(MousePosition { x, y }))
}
pub fn wheel_rotations(vertical: bool, rotation_units: i16) -> Self {
fn wheel_rotations(vertical: bool, rotation_units: i16) -> Self {
Self(Operation::WheelRotations(WheelRotations {
is_vertical: vertical,
rotation_units,
}))
}
pub fn key_pressed(scancode: u16) -> Self {
fn key_pressed(scancode: u16) -> Self {
Self(Operation::KeyPressed(Scancode::from_u16(scancode)))
}
pub fn key_released(scancode: u16) -> Self {
fn key_released(scancode: u16) -> Self {
Self(Operation::KeyReleased(Scancode::from_u16(scancode)))
}
pub fn unicode_pressed(unicode: char) -> Self {
fn unicode_pressed(unicode: char) -> Self {
Self(Operation::UnicodeKeyPressed(unicode))
}
pub fn unicode_released(unicode: char) -> Self {
fn unicode_released(unicode: char) -> Self {
Self(Operation::UnicodeKeyReleased(unicode))
}
}
#[wasm_bindgen]
pub struct InputTransaction(pub(crate) SmallVec<[Operation; 3]>);
pub(crate) struct InputTransaction(pub(crate) SmallVec<[Operation; 3]>);
#[wasm_bindgen]
impl InputTransaction {
pub fn init() -> Self {
impl iron_remote_desktop::InputTransaction for InputTransaction {
type DeviceEvent = DeviceEvent;
fn init() -> Self {
Self(SmallVec::new())
}
pub fn add_event(&mut self, event: DeviceEvent) {
fn add_event(&mut self, event: Self::DeviceEvent) {
self.0.push(event.0);
}
}
+19 -46
View File
@@ -12,6 +12,8 @@ extern crate time as _;
#[macro_use]
extern crate tracing;
use iron_remote_desktop::RemoteDesktopApi;
mod canvas;
mod clipboard;
mod error;
@@ -20,56 +22,27 @@ mod input;
mod network_client;
mod session;
use wasm_bindgen::prelude::*;
struct Api;
#[wasm_bindgen]
pub fn iron_init(log_level: &str) {
// When the `console_error_panic_hook` feature is enabled, we can call the
// `set_panic_hook` function at least once during initialization, and then
// we will get better error messages if our code ever panics.
//
// For more details see
// https://github.com/rustwasm/console_error_panic_hook#readme
#[cfg(feature = "panic_hook")]
console_error_panic_hook::set_once();
if let Ok(level) = log_level.parse::<tracing::Level>() {
set_logger_once(level);
}
impl RemoteDesktopApi for Api {
type Session = session::Session;
type SessionBuilder = session::SessionBuilder;
type SessionTerminationInfo = session::SessionTerminationInfo;
type DeviceEvent = input::DeviceEvent;
type InputTransaction = input::InputTransaction;
type ClipboardTransaction = clipboard::RdpClipboardTransaction;
type ClipboardContent = clipboard::RdpClipboardContent;
type Error = error::IronError;
}
fn set_logger_once(level: tracing::Level) {
use tracing_subscriber::filter::LevelFilter;
use tracing_subscriber::fmt::time::UtcTime;
use tracing_subscriber::prelude::*;
use tracing_web::MakeConsoleWriter;
static INIT: std::sync::Once = std::sync::Once::new();
INIT.call_once(|| {
let fmt_layer = tracing_subscriber::fmt::layer()
.with_ansi(false)
.with_timer(UtcTime::rfc_3339()) // std::time is not available in browsers
.with_writer(MakeConsoleWriter);
let level_filter = LevelFilter::from_level(level);
tracing_subscriber::registry().with(fmt_layer).with(level_filter).init();
#[doc(hidden)]
pub mod internal {
#[allow(dead_code)]
fn iron_init(log_level: &str) {
iron_remote_desktop::iron_init(log_level);
debug!("IronRDP is ready");
})
}
#[wasm_bindgen]
#[derive(Clone)]
pub struct DesktopSize {
pub width: u16,
pub height: u16,
}
#[wasm_bindgen]
impl DesktopSize {
pub fn init(width: u16, height: u16) -> Self {
DesktopSize { width, height }
}
}
iron_remote_desktop::export!(crate::Api);
+87 -93
View File
@@ -14,6 +14,7 @@ use futures_util::io::{ReadHalf, WriteHalf};
use futures_util::{select, AsyncWriteExt as _, FutureExt as _, StreamExt as _};
use gloo_net::websocket;
use gloo_net::websocket::futures::WebSocket;
use iron_remote_desktop::{CursorStyle, DesktopSize, IronErrorKind};
use ironrdp::cliprdr::backend::ClipboardMessage;
use ironrdp::cliprdr::CliprdrClient;
use ironrdp::connector::connection_activation::ConnectionActivationState;
@@ -31,24 +32,23 @@ use ironrdp_futures::{single_sequence_step_read, FramedWrite};
use rgb::AsPixels as _;
use serde::{Deserialize, Serialize};
use tap::prelude::*;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsValue;
use wasm_bindgen_futures::spawn_local;
use web_sys::HtmlCanvasElement;
use crate::canvas::Canvas;
use crate::clipboard::{ClipboardTransaction, WasmClipboard, WasmClipboardBackend, WasmClipboardBackendMessage};
use crate::error::{IronError, IronErrorKind};
use crate::clipboard;
use crate::clipboard::{RdpClipboardTransaction, WasmClipboard, WasmClipboardBackend, WasmClipboardBackendMessage};
use crate::error::IronError;
use crate::image::extract_partial_image;
use crate::input::InputTransaction;
use crate::network_client::WasmNetworkClient;
use crate::{clipboard, DesktopSize};
const DEFAULT_WIDTH: u16 = 1280;
const DEFAULT_HEIGHT: u16 = 720;
#[wasm_bindgen]
#[derive(Clone, Default)]
pub struct SessionBuilder(Rc<RefCell<SessionBuilderInner>>);
pub(crate) struct SessionBuilder(Rc<RefCell<SessionBuilderInner>>);
struct SessionBuilderInner {
username: Option<String>,
@@ -101,26 +101,28 @@ impl Default for SessionBuilderInner {
}
}
#[wasm_bindgen]
impl SessionBuilder {
pub fn init() -> SessionBuilder {
impl iron_remote_desktop::SessionBuilder for SessionBuilder {
type Session = Session;
type Error = IronError;
fn init() -> Self {
Self(Rc::new(RefCell::new(SessionBuilderInner::default())))
}
/// Required
pub fn username(&self, username: String) -> SessionBuilder {
fn username(&self, username: String) -> Self {
self.0.borrow_mut().username = Some(username);
self.clone()
}
/// Required
pub fn destination(&self, destination: String) -> SessionBuilder {
fn destination(&self, destination: String) -> Self {
self.0.borrow_mut().destination = Some(destination);
self.clone()
}
/// Optional
pub fn server_domain(&self, server_domain: String) -> SessionBuilder {
fn server_domain(&self, server_domain: String) -> Self {
self.0.borrow_mut().server_domain = if server_domain.is_empty() {
None
} else {
@@ -130,31 +132,31 @@ impl SessionBuilder {
}
/// Required
pub fn password(&self, password: String) -> SessionBuilder {
fn password(&self, password: String) -> Self {
self.0.borrow_mut().password = Some(password);
self.clone()
}
/// Required
pub fn proxy_address(&self, address: String) -> SessionBuilder {
fn proxy_address(&self, address: String) -> Self {
self.0.borrow_mut().proxy_address = Some(address);
self.clone()
}
/// Required
pub fn auth_token(&self, token: String) -> SessionBuilder {
fn auth_token(&self, token: String) -> Self {
self.0.borrow_mut().auth_token = Some(token);
self.clone()
}
/// Optional
pub fn desktop_size(&self, desktop_size: DesktopSize) -> SessionBuilder {
fn desktop_size(&self, desktop_size: DesktopSize) -> Self {
self.0.borrow_mut().desktop_size = desktop_size;
self.clone()
}
/// Optional
pub fn render_canvas(&self, canvas: HtmlCanvasElement) -> SessionBuilder {
fn render_canvas(&self, canvas: HtmlCanvasElement) -> Self {
self.0.borrow_mut().render_canvas = Some(canvas);
self.clone()
}
@@ -176,36 +178,36 @@ impl SessionBuilder {
/// - `none` (hide cursor); other arguments are `UNDEFINED`
/// - `url` (custom cursor data URL); `cursor_data` contains the data URL with Base64-encoded
/// cursor bitmap; `hotspot_x` and `hotspot_y` are set to the cursor hotspot coordinates.
pub fn set_cursor_style_callback(&self, callback: js_sys::Function) -> SessionBuilder {
fn set_cursor_style_callback(&self, callback: js_sys::Function) -> Self {
self.0.borrow_mut().set_cursor_style_callback = Some(callback);
self.clone()
}
/// Required.
pub fn set_cursor_style_callback_context(&self, context: JsValue) -> SessionBuilder {
fn set_cursor_style_callback_context(&self, context: JsValue) -> Self {
self.0.borrow_mut().set_cursor_style_callback_context = Some(context);
self.clone()
}
/// Optional
pub fn remote_clipboard_changed_callback(&self, callback: js_sys::Function) -> SessionBuilder {
fn remote_clipboard_changed_callback(&self, callback: js_sys::Function) -> Self {
self.0.borrow_mut().remote_clipboard_changed_callback = Some(callback);
self.clone()
}
/// Optional
pub fn remote_received_format_list_callback(&self, callback: js_sys::Function) -> SessionBuilder {
fn remote_received_format_list_callback(&self, callback: js_sys::Function) -> Self {
self.0.borrow_mut().remote_received_format_list_callback = Some(callback);
self.clone()
}
/// Optional
pub fn force_clipboard_update_callback(&self, callback: js_sys::Function) -> SessionBuilder {
fn force_clipboard_update_callback(&self, callback: js_sys::Function) -> Self {
self.0.borrow_mut().force_clipboard_update_callback = Some(callback);
self.clone()
}
pub fn extension(&self, value: JsValue) -> SessionBuilder {
fn extension(&self, value: JsValue) -> Self {
match serde_wasm_bindgen::from_value::<Extension>(value) {
Ok(value) => match value {
Extension::KdcProxyUrl(kdc_proxy_url) => self.0.borrow_mut().kdc_proxy_url = Some(kdc_proxy_url),
@@ -220,7 +222,7 @@ impl SessionBuilder {
self.clone()
}
pub async fn connect(&self) -> Result<Session, IronError> {
async fn connect(&self) -> Result<Self::Session, Self::Error> {
let (
username,
destination,
@@ -252,7 +254,7 @@ impl SessionBuilder {
pcb = inner.pcb.clone();
kdc_proxy_url = inner.kdc_proxy_url.clone();
client_name = inner.client_name.clone();
desktop_size = inner.desktop_size.clone();
desktop_size = inner.desktop_size;
render_canvas = inner.render_canvas.clone().context("render_canvas missing")?;
@@ -375,30 +377,17 @@ pub(crate) enum RdpInputEvent {
TerminateSession,
}
enum CursorStyle {
Default,
Hidden,
Url {
data: String,
hotspot_x: u16,
hotspot_y: u16,
},
}
#[wasm_bindgen]
pub struct SessionTerminationInfo {
pub(crate) struct SessionTerminationInfo {
reason: GracefulDisconnectReason,
}
#[wasm_bindgen]
impl SessionTerminationInfo {
pub fn reason(&self) -> String {
impl iron_remote_desktop::SessionTerminationInfo for SessionTerminationInfo {
fn reason(&self) -> String {
self.reason.to_string()
}
}
#[wasm_bindgen]
pub struct Session {
pub(crate) struct Session {
desktop_size: connector::DesktopSize,
input_database: RefCell<ironrdp::input::Database>,
writer_tx: mpsc::UnboundedSender<Vec<u8>>,
@@ -415,9 +404,53 @@ pub struct Session {
clipboard: RefCell<Option<Option<WasmClipboard>>>,
}
#[wasm_bindgen]
impl Session {
pub async fn run(&self) -> Result<SessionTerminationInfo, IronError> {
fn h_send_inputs(&self, inputs: smallvec::SmallVec<[FastPathInputEvent; 2]>) -> Result<(), IronError> {
if !inputs.is_empty() {
trace!("Inputs: {inputs:?}");
self.input_events_tx
.unbounded_send(RdpInputEvent::FastPath(inputs))
.context("Send input events to writer task")?;
}
Ok(())
}
fn set_cursor_style(&self, style: CursorStyle) -> Result<(), IronError> {
let (kind, data, hotspot_x, hotspot_y) = match style {
CursorStyle::Default => ("default", None, None, None),
CursorStyle::Hidden => ("hidden", None, None, None),
CursorStyle::Url {
data,
hotspot_x,
hotspot_y,
} => ("url", Some(data), Some(hotspot_x), Some(hotspot_y)),
};
let args = js_sys::Array::from_iter([
JsValue::from_str(kind),
JsValue::from(data),
JsValue::from_f64(hotspot_x.unwrap_or_default().into()),
JsValue::from_f64(hotspot_y.unwrap_or_default().into()),
]);
let _ret = self
.set_cursor_style_callback
.apply(&self.set_cursor_style_callback_context, &args)
.map_err(|e| anyhow::Error::msg(format!("set cursor style callback failed: {e:?}")))?;
Ok(())
}
}
impl iron_remote_desktop::Session for Session {
type SessionTerminationInfo = SessionTerminationInfo;
type InputTransaction = InputTransaction;
type ClipboardTransaction = RdpClipboardTransaction;
type Error = IronError;
async fn run(&self) -> Result<Self::SessionTerminationInfo, Self::Error> {
let rdp_reader = self
.rdp_reader
.borrow_mut()
@@ -705,42 +738,30 @@ impl Session {
})
}
pub fn desktop_size(&self) -> DesktopSize {
fn desktop_size(&self) -> DesktopSize {
DesktopSize {
width: self.desktop_size.width,
height: self.desktop_size.height,
}
}
pub fn apply_inputs(&self, transaction: InputTransaction) -> Result<(), IronError> {
fn apply_inputs(&self, transaction: Self::InputTransaction) -> Result<(), Self::Error> {
let inputs = self.input_database.borrow_mut().apply(transaction);
self.h_send_inputs(inputs)
}
pub fn release_all_inputs(&self) -> Result<(), IronError> {
fn release_all_inputs(&self) -> Result<(), Self::Error> {
let inputs = self.input_database.borrow_mut().release_all();
self.h_send_inputs(inputs)
}
fn h_send_inputs(&self, inputs: smallvec::SmallVec<[FastPathInputEvent; 2]>) -> Result<(), IronError> {
if !inputs.is_empty() {
trace!("Inputs: {inputs:?}");
self.input_events_tx
.unbounded_send(RdpInputEvent::FastPath(inputs))
.context("Send input events to writer task")?;
}
Ok(())
}
pub fn synchronize_lock_keys(
fn synchronize_lock_keys(
&self,
scroll_lock: bool,
num_lock: bool,
caps_lock: bool,
kana_lock: bool,
) -> Result<(), IronError> {
) -> Result<(), Self::Error> {
use ironrdp::pdu::input::fast_path::FastPathInput;
let event = ironrdp::input::synchronize_event(scroll_lock, num_lock, caps_lock, kana_lock);
@@ -755,7 +776,7 @@ impl Session {
Ok(())
}
pub fn shutdown(&self) -> Result<(), IronError> {
fn shutdown(&self) -> Result<(), Self::Error> {
self.input_events_tx
.unbounded_send(RdpInputEvent::TerminateSession)
.context("failed to send terminate session event to writer task")?;
@@ -763,7 +784,7 @@ impl Session {
Ok(())
}
pub async fn on_clipboard_paste(&self, content: ClipboardTransaction) -> Result<(), IronError> {
async fn on_clipboard_paste(&self, content: Self::ClipboardTransaction) -> Result<(), Self::Error> {
self.input_events_tx
.unbounded_send(RdpInputEvent::ClipboardBackend(
WasmClipboardBackendMessage::LocalClipboardChanged(content),
@@ -773,33 +794,7 @@ impl Session {
Ok(())
}
fn set_cursor_style(&self, style: CursorStyle) -> Result<(), IronError> {
let (kind, data, hotspot_x, hotspot_y) = match style {
CursorStyle::Default => ("default", None, None, None),
CursorStyle::Hidden => ("hidden", None, None, None),
CursorStyle::Url {
data,
hotspot_x,
hotspot_y,
} => ("url", Some(data), Some(hotspot_x), Some(hotspot_y)),
};
let args = js_sys::Array::from_iter([
JsValue::from_str(kind),
JsValue::from(data),
JsValue::from_f64(hotspot_x.unwrap_or_default().into()),
JsValue::from_f64(hotspot_y.unwrap_or_default().into()),
]);
let _ret = self
.set_cursor_style_callback
.apply(&self.set_cursor_style_callback_context, &args)
.map_err(|e| anyhow::Error::msg(format!("set cursor style callback failed: {e:?}")))?;
Ok(())
}
pub fn resize(
fn resize(
&self,
width: u32,
height: u32,
@@ -817,14 +812,13 @@ impl Session {
.expect("send resize event to writer task");
}
#[allow(clippy::unused_self)]
pub fn supports_unicode_keyboard_shortcuts(&self) -> bool {
fn supports_unicode_keyboard_shortcuts(&self) -> bool {
// RDP does not support Unicode keyboard shortcuts (When key combinations are executed, only
// plain scancode events are allowed to function correctly).
false
}
pub fn extension_call(_value: JsValue) -> Result<JsValue, IronError> {
fn extension_call(_value: JsValue) -> Result<JsValue, Self::Error> {
Ok(JsValue::null())
}
}
@@ -9,7 +9,7 @@
"Zacharia Ellaham",
"Alexandr Yusuk"
],
"description": "Web Component providing agnostic implementation for Iron Wasm base client",
"description": "RDP backend for iron-remote-desktop.",
"version": "0.0.0",
"type": "module",
"private": true,
@@ -8,7 +8,7 @@
"Vladislav Nikonov",
"Zacharia Ellaham"
],
"description": "Web Component providing agnostic implementation for Iron Wasm base client.",
"description": "RDP backend for iron-remote-desktop.",
"version": "0.13.1",
"main": "iron-remote-desktop-rdp.js",
"types": "index.d.ts",